# Variant Groups

Product options and modifiers

## GET /api/v1/variant-groups

List variant groups

List variant groups with optional filtering. Group customFields belong to variant_groups; variants[].customFields belong to each linked inventory item. See [Variant custom fields](/docs/variant-custom-fields).

**Permissions:** 🔵 `variant_groups.read`

Query parameters:
- `locationId` (number, optional) — Filter by location ID
- `type` ("EXTRA" | "VARIANT", optional) — Filter by group type
- `includeVariants` (boolean, optional) — Include variants in response
- `page` (number, optional) — Page number
- `limit` (number, optional) — Items per page

Response 200 — Default Response

```ts
{
  status: "success"
  data: {
    variantGroups: Array<{
        id: number
        locationId: number
        name: string
        type: "EXTRA" | "VARIANT"
        description?: string | null
        min: number
        max?: number | null
        isVisible: boolean
        useChildCoursing: boolean
        maxOptionsVisible: number
        variants: unknown[]
        scheduleRules?: unknown[] | null
        channelAvailability?: { … } | null
        posOverriddenFields?: unknown[]
        customFields?: unknown[]
        metadata?: { … } | null
      }>
    pagination: {
      page: number
      limit: number
      total: number
      totalPages: number
    }
  }
  meta: {
    requestId: string
    timestamp: string // date-time
    executionTimeMs: number
  }
}
```

## POST /api/v1/variant-groups

Create variant group

Create a new variant group with variants

**Permissions:** 🟢 `variant_groups.create`

Custom fields: send customFields: [{ "key": "your_field_key", "value": "your_value", "expectedVersion": 0 }]. Use either fieldId or a stable key. expectedVersion is optional; supply the last-read version to reject stale edits. Omitted fields are unchanged; null clears an optional value. See [Custom field inputs](/docs/custom-fields#values-on-normal-create-and-update-calls).

Request body:

```ts
{
  locationId: number
  name: string
  type: "EXTRA" | "VARIANT"
  description?: string | null
  min?: number
  max?: number | null
  isVisible?: boolean
  useChildCoursing?: boolean
  maxOptionsVisible?: number
  variants: Array<{
      customFields?: …[]
      name: string
      price?: number | null
      defaultSelection?: boolean
      description?: string | null
      receiptTitle?: string | null
      rank?: number | null
      itemId?: number | null
      quantity?: number
      allowQuantityChange?: boolean
      quantityCents?: number
      minQuantityCents?: number | null
      maxQuantityCents?: number | null
    }>
  customFields?: unknown | unknown[]
}
```

Response 201 — Default Response

```ts
{
  status: "success"
  data: {
    id: number
    locationId: number
    name: string
    type: "EXTRA" | "VARIANT"
    description?: string | null
    min: number
    max?: number | null
    isVisible: boolean
    useChildCoursing: boolean
    maxOptionsVisible: number
    variants: Array<{
        id: number
        variantGroupId: number
        name: string
        price?: number | null
        defaultSelection: boolean
        description?: string | null
        rank?: number | null
        itemId?: number | null
        quantity: number
        allowQuantityChange: boolean
        minQuantity?: number | null
        maxQuantity?: number | null
        quantityCents: number
        minQuantityCents?: number | null
        maxQuantityCents?: number | null
        posOverriddenFields?: unknown[]
        customFields?: unknown[]
        metadata?: { … } | null
      }>
    scheduleRules?: Array<{
        id?: string
        dayOfWeek?: number
        startTime?: string
        endTime?: string
        active?: boolean
      }> | null
    channelAvailability?: {
      byChannel?: {
        KIOSK?: boolean
        POS?: boolean
        ONLINE?: boolean
        TAKEAWAY?: boolean
        WOLT?: boolean
        FOODORA?: boolean
        UBER_EATS?: boolean
      }
      online?: boolean
      qr?: boolean
      kiosk?: boolean
      pos?: boolean
      wolt?: boolean
      foodora?: boolean
      uberEats?: boolean
    } | null
    posOverriddenFields?: string[]
    customFields?: Array<{
        fieldId?: string
        key?: string
        label?: string
        group?: string
        type?: string
        displayValue?: string
        value?: unknown // The stored value; its JSON type follows the field type
        version?: integer
        status?: string
      }>
    metadata?: {
      customFields?: Record<string, unknown>
    } | null
  }
  meta: {
    requestId: string
    timestamp: string // date-time
    executionTimeMs: number
  }
}
```

## GET /api/v1/variant-groups/{id}

Get variant group

Get a single variant group by ID with its variants. Group customFields belong to variant_groups; variants[].customFields belong to each linked inventory item. See [Variant custom fields](/docs/variant-custom-fields).

**Permissions:** 🔵 `variant_groups.read`

Path parameters:
- `id` (string, required) — Variant group ID

Response 200 — Default Response

```ts
{
  status: "success"
  data: {
    id: number
    locationId: number
    name: string
    type: "EXTRA" | "VARIANT"
    description?: string | null
    min: number
    max?: number | null
    isVisible: boolean
    useChildCoursing: boolean
    maxOptionsVisible: number
    variants: Array<{
        id: number
        variantGroupId: number
        name: string
        price?: number | null
        defaultSelection: boolean
        description?: string | null
        rank?: number | null
        itemId?: number | null
        quantity: number
        allowQuantityChange: boolean
        minQuantity?: number | null
        maxQuantity?: number | null
        quantityCents: number
        minQuantityCents?: number | null
        maxQuantityCents?: number | null
        posOverriddenFields?: unknown[]
        customFields?: unknown[]
        metadata?: { … } | null
      }>
    scheduleRules?: Array<{
        id?: string
        dayOfWeek?: number
        startTime?: string
        endTime?: string
        active?: boolean
      }> | null
    channelAvailability?: {
      byChannel?: {
        KIOSK?: boolean
        POS?: boolean
        ONLINE?: boolean
        TAKEAWAY?: boolean
        WOLT?: boolean
        FOODORA?: boolean
        UBER_EATS?: boolean
      }
      online?: boolean
      qr?: boolean
      kiosk?: boolean
      pos?: boolean
      wolt?: boolean
      foodora?: boolean
      uberEats?: boolean
    } | null
    posOverriddenFields?: string[]
    customFields?: Array<{
        fieldId?: string
        key?: string
        label?: string
        group?: string
        type?: string
        displayValue?: string
        value?: unknown // The stored value; its JSON type follows the field type
        version?: integer
        status?: string
      }>
    metadata?: {
      customFields?: Record<string, unknown>
    } | null
  }
  meta: {
    requestId: string
    timestamp: string // date-time
    executionTimeMs: number
  }
}
```

## PUT /api/v1/variant-groups/{id}

Update variant group

Update an existing variant group

**Permissions:** 🟠 `variant_groups.update`

Custom fields: send customFields: [{ "key": "your_field_key", "value": "your_value", "expectedVersion": 0 }]. Use either fieldId or a stable key. expectedVersion is optional; supply the last-read version to reject stale edits. Omitted fields are unchanged; null clears an optional value. See [Custom field inputs](/docs/custom-fields#values-on-normal-create-and-update-calls).

Path parameters:
- `id` (string, required) — Variant group ID

Request body:

```ts
{
  name?: string
  description?: string | null
  min?: number
  max?: number | null
  isVisible?: boolean
  useChildCoursing?: boolean
  maxOptionsVisible?: number
  variants?: Array<{
      customFields?: …[]
      name: string
      price?: number | null
      defaultSelection?: boolean
      description?: string | null
      receiptTitle?: string | null
      rank?: number | null
      itemId?: number | null
      quantity?: number
      allowQuantityChange?: boolean
      quantityCents?: number
      minQuantityCents?: number | null
      maxQuantityCents?: number | null
      id?: number
    }>
  customFields?: unknown | unknown[]
}
```

Response 200 — Default Response

```ts
{
  status: "success"
  data: {
    id: number
    locationId: number
    name: string
    type: "EXTRA" | "VARIANT"
    description?: string | null
    min: number
    max?: number | null
    isVisible: boolean
    useChildCoursing: boolean
    maxOptionsVisible: number
    variants: Array<{
        id: number
        variantGroupId: number
        name: string
        price?: number | null
        defaultSelection: boolean
        description?: string | null
        rank?: number | null
        itemId?: number | null
        quantity: number
        allowQuantityChange: boolean
        minQuantity?: number | null
        maxQuantity?: number | null
        quantityCents: number
        minQuantityCents?: number | null
        maxQuantityCents?: number | null
        posOverriddenFields?: unknown[]
        customFields?: unknown[]
        metadata?: { … } | null
      }>
    scheduleRules?: Array<{
        id?: string
        dayOfWeek?: number
        startTime?: string
        endTime?: string
        active?: boolean
      }> | null
    channelAvailability?: {
      byChannel?: {
        KIOSK?: boolean
        POS?: boolean
        ONLINE?: boolean
        TAKEAWAY?: boolean
        WOLT?: boolean
        FOODORA?: boolean
        UBER_EATS?: boolean
      }
      online?: boolean
      qr?: boolean
      kiosk?: boolean
      pos?: boolean
      wolt?: boolean
      foodora?: boolean
      uberEats?: boolean
    } | null
    posOverriddenFields?: string[]
    customFields?: Array<{
        fieldId?: string
        key?: string
        label?: string
        group?: string
        type?: string
        displayValue?: string
        value?: unknown // The stored value; its JSON type follows the field type
        version?: integer
        status?: string
      }>
    metadata?: {
      customFields?: Record<string, unknown>
    } | null
  }
  meta: {
    requestId: string
    timestamp: string // date-time
    executionTimeMs: number
  }
}
```

## DELETE /api/v1/variant-groups/{id}

Delete variant group

Delete a variant group and its variants

**Permissions:** 🔴 `variant_groups.delete`

Path parameters:
- `id` (string, required) — Variant group ID

Response 200 — Variant group successfully deleted

```ts
{
  status?: "success"
  data?: {
    deleted?: boolean
  }
  meta?: {
    requestId: string
    timestamp: string // date-time
    executionTimeMs: number
  }
}
```

## GET /api/v1/variants/{id}

Get variant custom fields

Read or patch the linked inventory item's stored fields through a variant. Requires variant_groups.read and inventory.read; writes also require inventory.update and API write channels. Values are shared by variants linking to the same item at this location. Group-level fields are a separate variant_groups object. See [Variant custom fields](/docs/variant-custom-fields). Set customFields using fieldId or key, value or reset, and optional expectedVersion.

Query parameters:
- `locationId` (integer, required)

Path parameters:
- `id` (integer, required)

Response 200 — Default Response

```ts
{
  status?: "success"
  data?: {
    id?: integer
    itemId?: integer
    locationId?: integer
    customFields?: Array<{
        fieldId?: string
        key?: string
        label?: string
        group?: string
        type?: string
        displayValue?: string
        value?: unknown // The stored value; its JSON type follows the field type
        version?: integer
        status?: string
      }>
    metadata?: {
      customFields?: Record<string, unknown>
    } | null
  }
}
```

## PATCH /api/v1/variants/{id}

Update variant custom fields

Read or patch the linked inventory item's stored fields through a variant. Requires variant_groups.read and inventory.read; writes also require inventory.update and API write channels. Values are shared by variants linking to the same item at this location. Group-level fields are a separate variant_groups object. See [Variant custom fields](/docs/variant-custom-fields). Set customFields using fieldId or key, value or reset, and optional expectedVersion.

Query parameters:
- `locationId` (integer, required)

Path parameters:
- `id` (integer, required)

Request body:

```ts
{
  customFields: unknown | unknown[]
}
```

Response 200 — Default Response

```ts
{
  status?: "success"
  data?: {
    id?: integer
    itemId?: integer
    locationId?: integer
    customFields?: Array<{
        fieldId?: string
        key?: string
        label?: string
        group?: string
        type?: string
        displayValue?: string
        value?: unknown // The stored value; its JSON type follows the field type
        version?: integer
        status?: string
      }>
    metadata?: {
      customFields?: Record<string, unknown>
    } | null
  }
}
```
