Product variants represent the purchasable versions of a product — different sizes, colors, or configurations. Each variant has its own SKU, price, stock level, and optional image.

## Get Variant

```
GET /api/v1/variants/:idOrSku
```

Returns a variant by UUID or SKU code, including product info and price.

### Query Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `currency` | `string` | Currency code for price resolution (e.g. `EUR`) |

```json
{
  "id": "0191abc0-0000-7000-8000-000000000100",
  "sku": "CT-SM-BLK",
  "barcode": "5901234123457",
  "description": "Charcoal colourway with a longer, cleaner burn.",
  "title": "Small / Black",
  "priceCents": 2500,
  "currency": "USD",
  "imageUrl": "https://cdn.example.com/tee-black.jpg",
  "stock": 42,
  "productId": "0191abc0-1234-7def-8000-000000000001",
  "product": {
    "id": "0191abc0-1234-7def-8000-000000000001",
    "name": "Classic Tee",
    "slug": "classic-tee",
    "images": ["https://cdn.example.com/tee.jpg"]
  }
}
```

`stock` is `null` when the merchant does not track inventory for the variant — it sells freely and has no quantity ceiling. Treat `null` as available, not as sold out; only `0` means sold out.

---

## Update Variant

```
PATCH /api/v1/variants/:idOrSku
```

Partially updates a variant by UUID or SKU. Only the provided fields are changed.

### Request Body

| Field | Type | Description |
|-------|------|-------------|
| `sku` | `string` | New SKU code |
| `barcode` | `string \| null` | Barcode (EAN, UPC, GTIN, ISBN, or internal). Pass `null` to clear. |
| `description` | `string \| null` | Per-variant description (markdown, max 4000 chars), shown on the product page below the product summary. Pass `null` to clear. |
| `title` | `string` | Variant display name (e.g. `Large / Red`) |
| `priceCents` | `number` | **Net** price in cents (e.g. `2999` = $29.99) |
| `priceGrossCents` | `number` | **Gross** price in cents — converted to net using the tax rate assigned to the variant's product. Send instead of `priceCents`, never both. |
| `prices` | `object` | Per-currency prices as decimals (e.g. `{ "EUR": 25.99 }`). Upserts by currency — an existing price for that currency is overwritten. The base/store currency is set via `priceCents`/`priceGrossCents`, not here. |
| `images` | `string[]` | Replace the variant's images. External URLs are uploaded to the store's CDN; URLs already on the CDN pass through. `[]` removes all images. |
| `imageUrl` | `string \| null` | **Deprecated** — prefer `images`. Single variant image URL (`null` to remove). External URLs are now uploaded to the store's CDN (previously stored verbatim). Cannot be combined with `images`. |
| `stock` | `number` | Inventory quantity. Setting it also puts the variant under inventory tracking — a variant created without `stock` sells freely until the first level is written here. |
| `options` | `object` | Option label → value map (e.g. `{ "Zapach": "Black Fig" }`). Replaces this variant's option set, rebuilding the variant types/values/combinations that drive the storefront picker. |
| `attributes` | `object[]` | Per-variant key/value rows shown on the product page, e.g. `[{ "key": "Nuty zapachowe", "value": "bergamotka, cedr" }]`. Each entry is `{ key, value, checked? }`; the array replaces whatever is stored. |

Sending both `images` and `imageUrl` returns `400`. Sending both `priceCents` and `priceGrossCents` is rejected too — pick one. The rate used for the gross → net conversion is resolved from the variant's product; with no rate assigned, gross and net are identical. Both fields are **cents** (integer minor units), unlike the decimal `price`/`priceGross` on `POST /api/v1/products` — see [Prices and tax](/docs/api-reference/products#prices-and-tax).

A `stock` change goes through the same inventory pipeline the admin uses: the movement is recorded on the variant's stock history, back-in-stock notification emails go out when the variant moves from zero to a positive level, and the cached storefront reads that embed stock are invalidated immediately.

```bash
curl -X PATCH \
  -H "Authorization: Bearer your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "priceCents": 2999,
    "stock": 100,
    "sku": "CT-SM-BLK-V2"
  }' \
  https://your-store.yns.store/api/v1/variants/CT-SM-BLK
```

To write the shopper-facing gross amount instead, send `priceGrossCents` — at 23% VAT, `3689` gross stores `2999` net:

```bash
curl -X PATCH \
  -H "Authorization: Bearer your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"priceGrossCents": 3689}' \
  https://your-store.yns.store/api/v1/variants/CT-SM-BLK
```

---

## Delete Variant

```
DELETE /api/v1/variants/:idOrSku
```

Permanently deletes a variant by UUID or SKU. Cannot delete the last variant of a product.

A **rolling subscription** programme is sold as exactly one price, so its variants are locked: deleting a variant whose product has `subscriptionMode: "rolling"` is refused with `409` and the code `rolling_variants_locked` in the `hint`. Change the programme's price in the admin under Subscriptions → Rolling instead.

Returns `204 No Content` on success.