variants field of a Product.
The variant model follows the UCP catalog lookup specification: a product carries a set of options (the dimensions a shopper can choose, like Color and Size), each option has a set of values (Blue, Red, XL), and every value carries the two availability signals UCP defines — exists and available. The selected array reflects the effective selection after the server resolves your request.
The variant data model
Product.variants is either null (the product has no variations) or a Variants object:
VariantOption renders as a selector row, selected marks the active configuration, and every value’s exists/available state styles its swatch or pill:

Variants
| Field | Type | Relevance |
|---|---|---|
options | VariantOption[] | Every dimension the product can be configured along. Render one selector (row of swatches or pills) per option. |
selected | SelectedOption[] | The effective selection after the server resolves your request — the configuration the returned product currently represents. Use it to highlight the active value in each selector. |
VariantOption
| Field | Type | Relevance |
|---|---|---|
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 — including values that don’t exist for the current selection (see exists). |
OptionValue
| Field | Type | Relevance |
|---|---|---|
label | string | The display value ("Blue", "XL"). This is also the value you send back when selecting (see Selecting a variant). |
exists | boolean | Whether this value forms a real variant given the other selected options. false means the combination isn’t offered (e.g. the shirt exists in XL, but not in this color + XL). Always present. See available vs exists. |
available | AvailabilityStatus | null | Stock status of this value. Not hydrated on /v1/search — it is always null on search results. |
thumbnail_url | string | null | A swatch image for this value (e.g. a color chip). When set, render the value as an image swatch rather than a text pill. |
product_id | string | null | When set, this value resolves to a different product. Selecting it should navigate to that product ID rather than re-fetching the current one. Pairs with thumbnail_url for swatch-style selectors. |
SelectedOption
| Field | Type | Relevance |
|---|---|---|
name | string | The option this selection applies to. Matches a VariantOption.name. |
label | string | The currently selected value for that option. Matches an OptionValue.label. |
AvailabilityStatus
available is one of the following stock statuses:
| 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 samevariants shape is returned by both endpoints, but available is only populated on product detail. This is the single most important difference to design around.
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 |
Honors option_* selection params | ❌ | ✅ |
GET /v1/products/{id} to get live available values (this call is free).
Python
Selecting a variant
To resolve a specific configuration, pass each chosen value toGET /v1/products/{id} as an option_<OptionName>=<Label> query parameter. The option name and label are taken verbatim from VariantOption.name and OptionValue.label.
cURL
productfields (title, price, image, offers) update to the matching variant.variants.selectedechoes the effective selection.
Always read selected after relaxation
If the exact combination you requested doesn’t exist, the server relaxes your selection to the closest valid variant instead of returning nothing. Relaxation tries to satisfy the values you sent, but there is no guarantee every selection is honored — when a combination can’t be satisfied, some of your selections may be dropped to land on a real variant. Because the outcome isn’t guaranteed to match what you sent, detect what actually happened by diffing your request against variants.selected:
Python
variants.selected, not from the values you sent — it’s the source of truth for what’s actually on screen.
Navigating across products
Some option values point at a separate product rather than reconfiguring the current one. These values have a non-nullproduct_id (and usually a thumbnail_url). When a shopper picks one, navigate to that product ID instead of appending an option_* param:
available vs. exists
exists and available describe two different kinds of “not quite,” and they should look different in the UI. Conflating them misleads shoppers — one means “you can’t buy this,” the other means “I’ll adjust your other choices to make this work.” Render values in three emphasis tiers, from full strength to faintest:
- Purchasable (
exists: true, in stock) — full emphasis. Solid border; the selected value gets a colored border and tint. - Out of stock (
exists: true,availableisSoldOut/OutOfStock/Discontinued) — dimmed. It’s a real, offered variant you simply can’t buy right now, so a muted/strike-through treatment is the right signal. (availableis only meaningful on product detail; it’snullfrom search.) - Not offered with current selection (
exists: false) — faintest of all. This does not mean the value is unavailable; it means this exact combination isn’t offered (e.g. you’ve selectedColor: Forest Greenand the green shirt was never made inXL). Make it the lightest element on screen — greyed text, muted fill, dashed border — so it clearly reads as “not a real option for what you’ve picked.” Crucially, keep it selectable: clicking it relaxes your other selections to land on a real variant.
— placeholder yet remain clickable.
| State | exists | available | Recommended styling |
|---|---|---|---|
| Purchasable | true | InStock / LimitedAvailability / PreOrder / BackOrder | Full emphasis. Selected → colored border + light tint. |
| Out of stock | true | SoldOut / OutOfStock / Discontinued | Dimmed (opacity-60 line-through). Keep it clickable to view; optionally add a “Sold out” tag. |
| Not offered with current selection | false | (null) | Lightest of all — border-dashed, muted fill (bg-muted/30), faded text (text-muted-foreground/40), with a — in place of any detail. Still fully clickable. |
| Stock unknown | true | null (search results) | Full emphasis; fetch product detail for live stock before checkout. |
disabled:
- Three tiers, never disabled —
valueStatecollapsesexists+availableinto one of four labels, and thePILL/SWATCHlookups give each tier its own weight. Every value stays clickable so server-side relaxation can do its job. - Faintest = not offered —
border-dashed+bg-muted/30+text-muted-foreground/40(and a—placeholder) make non-existent values the lightest thing on screen, clearly weaker than the dimmed out-of-stock tier. - Swatches vs. pills — values with a
thumbnail_urlrender as image swatches; everything else is a text pill. Driven entirely bythumbnail_url, so you never need to know the underlying option type.
Summary
Product.variantscarriesoptions(dimensions → values) andselected(the effective configuration).- Each
OptionValueexposesexists(is this combination offered?) andavailable(is it in stock?) — design your UI around both. availableis only hydrated onGET /v1/products/{id}; it’snullonPOST /v1/search.- Select a configuration with
option_<Name>=<Label>query params on product detail; the server relaxes invalid combinations, so always trustvariants.selected. - A value with
product_idset points to a different product — navigate to it instead of re-resolving in place. - Render
thumbnail_urlvalues as swatches, others as pills; dim non-existent (exists: false) and out-of-stock values while keeping them clickable.