Product Variants Service
Overview
Product Variants represent distinct purchasable versions of a product.
A variant is a unique combination of attribute values (e.g. a specific option set such as Blue + Small) that results in a distinct stock-keeping unit. Each variant:
- belongs to a parent product
- carries its own SKU, title and weight
- can be independently activated or deactivated
- is linked to its attributes via
ProductVariantsAttributes(e.g. dimension assignments) - can have its own set of images via
ProductImages
The variants model is intentionally flat and closely mirrors the database table. Attributes and images are managed through their own dedicated services and are composed into the full product response by the Products service.
All operations are tenant-aware and scoped to the current tenant.
Endpoints
GET
/api/v1/ProductVariants/product/{productId}
Returns all variants for a given product.
Behavior:
- Returns records ordered by
Sort Order - Returns flat variant data only (no attributes, no images)
- For full nested output including attributes and images, use the Products endpoint
- Returns
404if no variants exist for the product
Authorization:
- No authentication required
GET
/api/v1/ProductVariants/{id}
Returns a single product variant by its identifier.
Behavior:
- Looks up variant by
Id - Returns flat variant data only
- Returns
404if the variant does not exist
Authorization:
- No authentication required
POST
/api/v1/ProductVariants
Creates a new product variant.
Request body:
Product Id(long, required) - the parent productSKU(string, required) - unique stock-keeping unit codeTitle(string, required) - variant display nameWeight Grams(int, optional) - variant weight in gramsIs Active(bool, optional) - defaults to database default (1)Sort Order(int, optional) - display order
Behavior:
- Validation is handled in the service layer
- SKU must be unique
- Successful creation writes an audit log entry
- To assign dimension attributes to the variant, use the
ProductVariantsAttributesservice after creation
Errors:
- Returns
400if validation fails
Authorization:
- Requires Bearer Token
PUT
/api/v1/ProductVariants/{id}
Updates an existing product variant.
Request:
- Variant ID is taken from the route
- Body must include:
Id(long, required)Product Id(long, required)SKU(string, required)Title(string, required)
- Optional:
Weight Grams(int)Is Active(bool)Sort Order(int)
Behavior:
- All provided fields overwrite existing values
- Successful update writes an audit log entry
Errors:
- Returns
400if validation fails or variant does not exist
Authorization:
- Requires Bearer Token
DELETE
/api/v1/ProductVariants/{id}
Deletes a product variant by its identifier.
Behavior:
- Performs a hard delete
- Record is permanently removed from the database
- Successful deletion writes an audit log entry
Errors:
- Returns
400if variant does not exist or deletion fails
Authorization:
- Requires Bearer Token
Models
VariantResponseModel
Returned as part of ProductModel.Variants by the Products service.
Fields:
Id (type: long)— internal identifierSKU (type: string?)— internal variant SKU (prefixed with{entityId}_for uniqueness)DisplaySku (type: string?)— the original supplier SKU without the entity prefix; use this for displayTitle (type: string?)— variant display nameIsActive (type: bool)— whether the variant is activeSortOrder (type: int)— display orderReOrder (type: int?)— reorder thresholdPropertyValues (type: List<VariantPropertyValueModel>)— resolved dimension/attribute valuesPricing (type: List<VariantPriceEntryModel>)— price tiers for this variantStockItems (type: List<VariantStockItemModel>)— stock levels per warehouseImages (type: List<ProductImageModel>?)— variant-specific images
VariantPropertyValueModel
Fields:
PropertyId (type: long)— property identifierPropertyName (type: string?)— property label (e.g. "Colour", "Size")Value (type: string?)— raw valueDisplayValue (type: string?)— formatted display value
VariantPriceEntryModel
Fields:
ProductPriceId (type: long)— price record identifierMinQty (type: decimal)— minimum quantity for this tierUnitPrice (type: decimal)— cost price (buy price)DefaultMarkupPer (type: decimal?)— default markup percentageDefaultSalesPrice (type: decimal?)— calculated default sale priceDefaultCustomisationPrice (type: decimal?)— additional customisation charge per unitEntityPriceOverride (type: PriceOverrideItemModel?)— entity-specific price override (populated when request includes anentityId)
PriceOverrideItemModel
Fields:
UnitPrice (type: decimal?)— overridden cost priceDefaultSalePrice (type: decimal?)— overridden sale priceDefaultMarkupPer (type: decimal?)— overridden markup percentageDefaultCustomisationPrice (type: decimal?)— overridden customisation priceStartDate (type: DateOnly?)— override valid fromEndDate (type: DateOnly?)— override valid until
VariantStockItemModel
Fields:
WarehouseId (type: long)— warehouse identifierWarehouse (type: string?)— warehouse nameQuantityOnHand (type: decimal)— current stock on handQuantityAllocated (type: decimal)— allocated quantity (reserved for orders)ReorderLevel (type: decimal?)— reorder thresholdLocation (type: string?)— bin / shelf location
Notes
- Validation is enforced in the service layer, not via model attributes
- Variant endpoints return flat data (1:1 with the database table)
- Full nested output (with attributes, images, pricing, and stock) is only available via the Products service
- Attribute assignment is managed via
ProductVariantsAttributes - Image assignment is managed via
ProductImages - All state-changing operations are audited
- Internal errors are logged but not exposed to clients