Variants & Axes
Retail variation is axis-driven. An axis like "Size" or "Color" is defined once at the organization level and reused across the whole catalog. Each product declares which axes apply to it, and each variant is one unique combination of axis values — a single SKU with its own price and stock. A T-shirt uses Size × Color; a laptop uses Storage × RAM; the model is the same.
Variant axes and product variants are managed through the TypeScript SDK (client.productVariantAxes, client.productVariants). The Python product guide manages pricing and stock directly on the product (stock_quantity, price); the schema below is the platform model.
Variant axes
An axis is defined once and reused. Its type controls how its values render.
import { VariantAxisType } from 'wiil-core-js';
const sizeAxis = await client.productVariantAxes.create({
name: 'Size',
type: VariantAxisType.TEXT,
values: [
{ id: 'sm', label: 'Small', sortOrder: 0 },
{ id: 'md', label: 'Medium', sortOrder: 1 },
{ id: 'lg', label: 'Large', sortOrder: 2 },
],
isActive: true,
});
const colorAxis = await client.productVariantAxes.create({
name: 'Color',
type: VariantAxisType.SWATCH,
values: [
{ id: 'black', label: 'Black', swatchColor: '#000000', sortOrder: 0 },
{ id: 'white', label: 'White', swatchColor: '#FFFFFF', sortOrder: 1 },
],
isActive: true,
});
const axis = await client.productVariantAxes.get('axis_123');
const byName = await client.productVariantAxes.getByName('Size');
const all = await client.productVariantAxes.list();
await client.productVariantAxes.update('axis_123', { id: 'axis_123', values: [/* ... */] });
await client.productVariantAxes.delete('axis_123');
VariantAxis fields
| Field | Type | Required | Description |
|---|---|---|---|
id | string | auto | Unique identifier |
name | string | Yes | Axis name ("Size", "Color", "Storage") |
type | enum | Yes | text, swatch, image, or numeric |
values | array | Yes | Axis values (min 1) |
isActive | boolean | No | Whether the axis is active (default true) |
VariantAxisValue
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique value ID |
label | string | Yes | Display label ("Small", "Red", "256GB") |
swatchColor | string | null | No | Hex color for swatch type |
imageId | string | null | No | Image for image type |
numericValue | number | null | No | Numeric value for numeric type |
sortOrder | integer | No | Display order (default 0) |
Product variants
A variant is a SKU: a specific combination of axis values, with fields that override the parent product (null = inherit).
const variant = await client.productVariants.create({
productId: 'product_123',
axisValues: { Size: 'Extra Large', Color: 'Blue' },
sku: 'TS-XL-BL',
price: 29.99,
isDefault: false,
isActive: true,
});
const loaded = await client.productVariants.get('variant_123');
const bySku = await client.productVariants.getBySku('TS-XL-BL');
const defaultVariant = await client.productVariants.getDefault('product_123');
const updated = await client.productVariants.update('variant_123', { id: 'variant_123', price: 32.99, isActive: true });
const batch = await client.productVariants.createBatch([
{ productId: 'product_123', axisValues: { Size: 'Medium', Color: 'Red' }, sku: 'TS-MD-RD', price: 24.99, isDefault: false, isActive: true },
{ productId: 'product_123', axisValues: { Size: 'Large', Color: 'Green' }, sku: 'TS-LG-GN', price: 26.99, isDefault: false, isActive: true },
]);
await client.productVariants.delete('variant_123');
ProductVariant fields
| Field | Type | Required | Description |
|---|---|---|---|
id | string | auto | Unique identifier |
productId | string | Yes | Parent product |
axisValues | object | Yes | Map of axis ID → selected value ID |
sku | string | null | No | SKU (overrides parent) |
barcode / partNumber / globalTradeItemNumber | string | null | No | Identifiers |
price | number | null | No | Variant price (null = inherit from product) |
cost / compareAtPrice | number | null | No | Cost and sale-display price |
stockQuantity | integer | null | No | Per-variant stock |
lowStockThreshold | integer | null | No | Per-variant low-stock threshold |
inventoryUnit | enum | null | No | EACH, CASE, PACK, BOX, PALLET, … |
weight / dimensions | — | No | Shipping overrides |
imageId / imageIds | — | No | Variant imagery |
channelMappings | array | null | No | Per-channel external variant IDs |
isActive | boolean | No | Available for sale (default true) |
isDefault | boolean | No | Default variant (default false) |
Stock status
Each variant resolves a stockStatus from its quantity and threshold:
| Value | Meaning |
|---|---|
in_stock | Available for purchase (or inventory not tracked) |
low_stock | At or below the low-stock threshold |
out_of_stock | Zero stock quantity |
Product axis bindings
A ProductAxisBinding records that a product uses a given axis, with a displayOrder controlling the order axes appear on the product. Each binding sets productId, axisId, displayOrder, and isActive.
Query options
| Resource | Filters | Sort fields |
|---|---|---|
VariantAxis | search, type, isActive | name, createdAt |
ProductVariant | productId, axisValueId, sku, isActive, inStock | sku, price, stockQuantity, createdAt |
Define axes once at the organization level and bind them to products; track stock per variant (stockQuantity) so fulfillment is accurate; and use the override pattern only where a variant truly differs from its parent.