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

# Create/Replace Product Variants

> Fully replace the variant tree for a product — this is a destructive full-sync, not an incremental add.

# Create/Replace Product Variants

Sets the complete variant tree (variations → optional sub-variations → sub-variants) for a product. This is a **full replace**, not an append: any existing variant nodes not present in your request are **deleted**.

<Warning>
  **Orphan cleanup is real and destructive.** If you fetch the current tree via [Get Variants](/api-reference/marketplace/get-variants), remove one variant client-side, and POST the rest, the removed variant (and any of its sub-variants) is permanently deleted server-side — there's no soft-delete or undo. Always send the **entire desired tree**, not a partial update.
</Warning>

## Path Parameters

<ParamField path="productId" type="integer" required>
  A product you own, or a product owned by an active sub-merchant in your marketplace (marketplace operators can manage sub-merchant product variants directly).
</ParamField>

## Request Body

<ParamField body="variation_type" type="string" required>
  Label for the top-level variation axis, e.g. `"size"`.
</ParamField>

<ParamField body="variation_custom_label" type="string">
  Display label shown to customers. Defaults to `variation_type` if omitted.
</ParamField>

<ParamField body="has_sub_variants" type="boolean">
  Whether each variation has a nested sub-variation axis (e.g. size → color). **Recalculated server-side after processing** — the server checks whether any `variations[].sub_variation.sub_variants` actually has entries and overwrites whatever you sent with that computed value. Sending `true` with no real sub-variants will be silently corrected to `false`.
</ParamField>

<ParamField body="total_quantity" type="integer" default="0">
  Only used if you provide it **and** it's truthy — otherwise it's computed server-side as the sum of each variation's `variation_quantity`. Either way, a second pass then recomputes the true total from the full tree (including sub-variant quantities) and writes that final number back to both the variation tree and the product record — so don't rely on the value you sent surviving unchanged.
</ParamField>

<ParamField body="variations" type="array" required>
  Non-empty array. Each item:

  <Expandable title="Variation item fields">
    <ParamField body="id" type="string" required>
      **You must generate this UUID client-side** — it is not server-assigned. Reuse the same `id` on subsequent POSTs to update rather than replace a specific node; omit it (or use a new one) to create.

      <Warning>
        If any `variation.id` (or nested `sub_variation`/`sub_variants` id) is missing, the whole request fails — but the response only ever says `"Failed to save variants"` with no field-level detail. The actual validation reason ("Variation ID is required") is logged server-side only, not returned to you. Double-check every node has an `id` before sending.
      </Warning>
    </ParamField>

    <ParamField body="sku" type="string">SKU for this variant.</ParamField>
    <ParamField body="variant_label" type="string">Display label, e.g. `"Medium"`.</ParamField>
    <ParamField body="variant_object" type="string">JSON-encoded string (not a nested object) describing the variant's attributes, e.g. `"{\"size\":\"M\"}"`.</ParamField>
    <ParamField body="variation_quantity" type="integer">Stock quantity at this level.</ParamField>
    <ParamField body="variation_add_price" type="integer">Price delta in smallest currency unit, added to the base product price.</ParamField>
    <ParamField body="variation_cost_price" type="integer" default="0">Cost price in smallest currency unit.</ParamField>
    <ParamField body="order_index" type="integer">Display order. Defaults to array index if omitted.</ParamField>

    <ParamField body="sub_variation" type="object">
      One nested sub-variation axis (e.g. color within size). Same `id`-required rule applies to it and its `sub_variants`.
    </ParamField>
  </Expandable>
</ParamField>

## Response

<Warning>
  The response `data` here is a **flat `ProductVariation` record** — it does not include the nested `variations`/`sub_variation`/`sub_variants` tree. If you need the full tree back (e.g. to get server-confirmed nesting), call [Get Variants](/api-reference/marketplace/get-variants) right after. This asymmetry between POST and GET response shapes is intentional in the current implementation, not a bug — don't rely on the POST response for anything beyond `id`/`total_quantity`.
</Warning>

```json theme={null}
{
  "success": true,
  "message": "Variants saved successfully",
  "data": {
    "id": "b6b6c2b0-...-uuid",
    "product_id": 501,
    "variation_type": "size",
    "variation_custom_label": "Size",
    "has_sub_variants": true,
    "total_quantity": 40
  }
}
```

## Side Effects

* Recomputes and overwrites `total_quantity` on both the variation tree and the parent `Product` record.
* Sets `Product.has_variation = true` unconditionally, even if `variations` ends up empty after processing.
* Deletes any variant/sub-variation/sub-variant rows not present in this request's `variations` array (see warning above).

## Error Codes

| Status | Error Code                 | Cause                                                                                                                                                           |
| ------ | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `404`  | `PRODUCT_NOT_FOUND`        | Product doesn't exist, or you don't own it and aren't its marketplace operator. (Deliberately not distinguished from "doesn't exist" — no ownership info leak.) |
| `400`  | `VALIDATION_MISSING_FIELD` | `variation_type` missing, or `variations` missing/empty.                                                                                                        |
| `400`  | `INTERNAL_ERROR`           | Generic failure — includes the missing-`id` case above; check every node has an `id` before retrying.                                                           |

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST https://api.khaime.com/api/v1/partner/marketplace/products/501/variants \
    -H "X-API-Key: pk_sandbox_your_key" \
    -H "Content-Type: application/json" \
    -d '{
      "variation_type": "size",
      "variation_custom_label": "Size",
      "has_sub_variants": false,
      "variations": [
        {
          "id": "v-11111111-1111-1111-1111-111111111111",
          "sku": "SHIRT-M",
          "variant_label": "Medium",
          "variant_object": "{\"size\":\"M\"}",
          "variation_quantity": 20,
          "variation_add_price": 0,
          "order_index": 0
        },
        {
          "id": "v-22222222-2222-2222-2222-222222222222",
          "sku": "SHIRT-L",
          "variant_label": "Large",
          "variant_object": "{\"size\":\"L\"}",
          "variation_quantity": 20,
          "variation_add_price": 200,
          "order_index": 1
        }
      ]
    }'
  ```
</RequestExample>
