> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://channel3.ferndocs.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://channel3.ferndocs.com/_mcp/server.

# Variants

Many products come in more than one configuration: a shirt in several colors and sizes, a shoe in multiple widths, a phone in different storage tiers. Channel3 models these as **variants**, exposed on the `variants` field of a Product.

## The variant data model

`Product.variants` is either `null` (the product has no variations) or a `Variants` object:

```json
{
  "variants": {
    "options": [
      {
        "name": "Color",
        "values": [
          {
            "label": "Midnight Blue",
            "exists": true,
            "available": "InStock",
            "thumbnail_url": "https://cdn.trychannel3.com/asdf",
            "product_id": "x8k2mq4"
          },
          {
            "label": "Forest Green",
            "exists": true,
            "available": "OutOfStock",
            "thumbnail_url": "https://cdn.trychannel3.com/abcd",
            "product_id": "p3n7wz9"
          }
        ]
      },
      {
        "name": "Size",
        "values": [
          { "label": "S",  "exists": true,  "available": "InStock" },
          { "label": "M",  "exists": true,  "available": "InStock" },
          { "label": "XL", "exists": false, "available": null }
        ]
      }
    ],
    "selected": [
      { "name": "Color", "label": "Midnight Blue" },
      { "name": "Size",  "label": "M" }
    ]
  }
}
```

### Variants

| Field      | Type               | Description                                                                                           |
| ---------- | ------------------ | ----------------------------------------------------------------------------------------------------- |
| `options`  | `VariantOption[]`  | Every dimension the product can be configured along. Render one selector per option.                  |
| `selected` | `SelectedOption[]` | The effective selection after the server resolves your request. Use it to highlight the active value. |

### VariantOption

| Field    | Type            | Description                                                              |
| -------- | --------------- | ------------------------------------------------------------------------ |
| `name`   | `string`        | The dimension name (`"Color"`, `"Size"`). Use as the selector's label.   |
| `values` | `OptionValue[]` | Every possible value for this dimension across the whole product family. |

### OptionValue

| Field           | Type                         | Description                                                                          |
| --------------- | ---------------------------- | ------------------------------------------------------------------------------------ |
| `label`         | `string`                     | The display value (`"Blue"`, `"XL"`). Also the value you send when selecting.        |
| `exists`        | `boolean`                    | Whether this value forms a real variant given the other selected options.            |
| `available`     | `AvailabilityStatus \| null` | Stock status. Always `null` on search results — only hydrated on product detail.     |
| `thumbnail_url` | `string \| null`             | A swatch image for this value. When set, render as an image swatch rather than text. |
| `product_id`    | `string \| null`             | When set, this value resolves to a different product. Navigate to that ID instead.   |

### AvailabilityStatus

| Value                 | Meaning                              |
| --------------------- | ------------------------------------ |
| `InStock`             | Purchasable now.                     |
| `LimitedAvailability` | Purchasable, low stock.              |
| `PreOrder`            | Orderable ahead of release.          |
| `BackOrder`           | Orderable, ships when restocked.     |
| `SoldOut`             | Temporarily unpurchasable.           |
| `OutOfStock`          | Not currently purchasable.           |
| `Discontinued`        | No longer offered.                   |
| `Unknown`             | Stock state could not be determined. |

## Search vs. product detail

The same `variants` shape is returned by both endpoints, but `available` is only populated on product detail.

|                               | `POST /v1/search` | `GET /v1/products/{id}` |
| ----------------------------- | ----------------- | ----------------------- |
| `options`, `values`, `labels` | Full set          | Full set                |
| `exists`                      | Populated         | Populated               |
| `thumbnail_url`, `product_id` | Populated         | Populated               |
| `available`                   | Always `null`     | Hydrated per value      |

Search is optimized for breadth — it returns the full option matrix so you can render selectors immediately, but it does not compute per-value stock. When you display a product for purchase, refetch it with `GET /v1/products/{id}` to get live `available` values (this call is free).

**`Python`**

```python title="Python"
from channel3_sdk import Channel3

client = Channel3()

# 1. Discover — variants present, but `available` is null on every value
results = client.products.search(query="merino wool sweater")
product = results.products[0]

for option in product.variants.options:
    print(option.name, [v.label for v in option.values])

# 2. Display — refetch to hydrate `available`
detail = client.products.retrieve(product.id)
for option in detail.variants.options:
    for value in option.values:
        print(option.name, value.label, value.exists, value.available)
```

**`TypeScript`**

```typescript title="TypeScript"
import { Channel3 } from "@channel3/sdk";

const client = new Channel3();

// 1. Discover — variants present, but `available` is null on every value
const results = await client.products.search({ query: "merino wool sweater" });
const product = results.products[0];

for (const option of product.variants.options) {
  console.log(option.name, option.values.map(v => v.label));
}

// 2. Display — refetch to hydrate `available`
const detail = await client.products.retrieve(product.id);
for (const option of detail.variants.options) {
  for (const value of option.values) {
    console.log(option.name, value.label, value.exists, value.available);
  }
}
```

## Selecting a variant

To resolve a specific configuration, pass each chosen value to `GET /v1/products/{id}` as an `option_<Name>=<Label>` query parameter:

```bash
curl "https://api.trychannel3.com/v1/products/x8k2mq4?option_Color=Forest%20Green&option_Size=M" \
  -H "x-api-key: $CHANNEL3_API_KEY"
```

The response reflects your selection in two places:

* `product` fields (title, price, image, offers) update to the matching variant.
* `variants.selected` echoes the effective selection.

### Relaxation

If the exact combination you requested doesn't exist, the server relaxes your selection to the closest valid variant instead of returning nothing. Always read `variants.selected` after the call to detect what actually happened:

**`Python`**

```python title="Python"
requested = {"Color": "Forest Green", "Size": "XL"}

detail = client.products.retrieve("x8k2mq4")
effective = {s.name: s.label for s in detail.variants.selected}

for name, label in requested.items():
    if effective.get(name) != label:
        print(f"{name}: requested {label!r}, resolved to {effective[name]!r}")
```

**`TypeScript`**

```typescript title="TypeScript"
const requested = { Color: "Forest Green", Size: "XL" };

const detail = await client.products.retrieve("x8k2mq4");
const effective = Object.fromEntries(
  detail.variants.selected.map(s => [s.name, s.label])
);

for (const [name, label] of Object.entries(requested)) {
  if (effective[name] !== label) {
    console.log(`${name}: requested '${label}', resolved to '${effective[name]}'`);
  }
}
```

## Navigating across products

Some option values point at a separate product rather than reconfiguring the current one. These values have a non-null `product_id` (and usually a `thumbnail_url`). When a shopper picks one, navigate to that product ID:

```typescript
function handleSelect(option: VariantOption, value: OptionValue) {
  if (value.product_id && value.product_id !== currentProductId) {
    // This value is a different product — navigate to it
    navigate(`/products/${value.product_id}`);
  } else {
    // Same product, different configuration — re-resolve with option params
    setSelection({ ...selection, [option.name]: value.label });
  }
}
```

## `available` vs. `exists`

`exists` and `available` describe two different things:

1. **Purchasable** (`exists: true`, in stock) — Full emphasis. The selected value gets a colored border.
2. **Out of stock** (`exists: true`, `available` is `SoldOut`/`OutOfStock`/`Discontinued`) — Dimmed. It's a real variant you can't buy right now.
3. **Doesn't exist** (`exists: false`) — Faintest. Selecting it would change another option. Show as a faint, disabled swatch.