Variant Groups API
Link catalog products that are the same device in different finishes, and manage the members of each link.
A variant group is a named link between catalog products that are the same device in different finishes, for example a ceiling speaker sold in white and in black. Every member of a group is a catalog product of the same make. The group's optionName says what differs between the members, for example Colour. Each member's variantLabel is that member's value for the option, for example White.
Rules
- A group holds one make. The group takes its
make from its products, so no request sends it.
- A product belongs to one group at most. Remove it from its group before you add it to another.
- Creating a group takes two or more products.
- A group's
name is unique within its make. The comparison ignores letter case and surrounding spaces.
- Removing the last member of a group deletes the group.
- Deleting a group or removing a member never deletes a product. The product stays in the catalog and loses its group and its label.
- Each write is checked in full before anything is saved. When a rule is broken, nothing is written and the response lists every problem under
VARIANT_GROUP_RULE.
- Text values are trimmed. A
name holds up to 100 characters, an optionName up to 50, a description up to 500 and a variantLabel up to 50.
To copy connection metadata from one member to the others, use POST /api/v1/products/{id}/metadata/apply-to-variants in the Products API.
GET /api/v1/variant-groups
Returns the variant groups in the catalog, ordered by name.
Query Parameters
| Field |
Type |
Required |
Description |
search |
string |
No |
Matches the group name or the make. The match ignores letter case and finds the text anywhere in either value. Leave it out to list every group. |
limit |
integer |
No |
The most groups to return. The default is 50. A value above 500 is read as 500. A value below 1 is read as 20. |
Example Request
GET /api/v1/variant-groups?search=biamp&limit=10
Authorization: Bearer YOUR_JWT_TOKEN
Response (200 - Success)
{
"success": true,
"data": [
{
"id": 12,
"make": "Biamp",
"name": "C-IC6LP",
"optionName": "Colour",
"memberCount": 2,
"labels": ["Black", "White"]
}
],
"message": "Retrieved 1 variant groups",
"timestamp": "2026-10-08T15:30:00Z"
}
| Field |
Type |
Description |
id |
integer |
The variant group's ID |
make |
string |
The make that every member shares |
name |
string |
The group name |
optionName |
string |
What differs between the members, for example Colour |
memberCount |
integer |
The number of products in the group |
labels |
array of string |
The members' labels, ordered by the members' model numbers |
The response has no paging and no total count. When it holds exactly limit groups, more groups may exist. Send a narrower search to reach them.
GET /api/v1/variant-groups/{id}
Returns one variant group with its members, ordered by model number.
Path Parameters
| Field |
Type |
Required |
Description |
id |
integer |
Yes |
The variant group's ID |
Example Request
GET /api/v1/variant-groups/12
Authorization: Bearer YOUR_JWT_TOKEN
Response (200 - Success)
{
"success": true,
"data": {
"id": 12,
"make": "Biamp",
"name": "C-IC6LP",
"optionName": "Colour",
"description": "6.5-inch low-profile ceiling speaker",
"members": [
{
"productId": 413,
"model": "C-IC6LP-B",
"part": "C-IC6LP-B",
"variantLabel": "Black",
"catalogPrice": 205.00,
"vendor": "ADI Global",
"productType": "Ceiling Speaker",
"hasMetadata": false
},
{
"productId": 412,
"model": "C-IC6LP-W",
"part": "C-IC6LP-W",
"variantLabel": "White",
"catalogPrice": 212.00,
"vendor": "ADI Global",
"productType": "Ceiling Speaker",
"hasMetadata": true
}
]
},
"message": null,
"timestamp": "2026-10-08T15:30:00Z"
}
| Field |
Type |
Description |
description |
string or null |
The group's description |
members[].productId |
integer |
The member's catalog product ID |
members[].model |
string |
The product's model number |
members[].part |
string or null |
The product's part number |
members[].variantLabel |
string |
The member's value for the group's option |
members[].catalogPrice |
number or null |
The product's catalog price |
members[].vendor |
string or null |
The product's vendor |
members[].productType |
string or null |
The product's type |
members[].hasMetadata |
boolean |
true when the product has connection metadata |
The fields id, make, name and optionName have the same meaning as in the list. Every endpoint below that returns a group returns this shape.
Error Responses
| Status | Code | Description |
| 404 | VARIANT_GROUP_NOT_FOUND | No variant group has this ID |
POST /api/v1/variant-groups
Creates a variant group from two or more products. The products must be of one make, and none of them may already be in a group.
Request Body
| Field |
Type |
Required |
Description |
name |
string |
Yes |
The group name, up to 100 characters. It is usually the part of the model numbers that the members share. It must be unique within the make. |
optionName |
string |
Yes |
What differs between the members, for example Colour or Finish. Up to 50 characters. |
description |
string |
No |
A description of the group, up to 500 characters |
members |
array |
Yes |
The products to group. The list must name two or more different products. |
members[].productId |
integer |
Yes |
A catalog product ID. A product may be listed once. |
members[].variantLabel |
string |
Yes |
The product's value for the option, for example White. Up to 50 characters. |
Example Request
{
"name": "C-IC6LP",
"optionName": "Colour",
"description": "6.5-inch low-profile ceiling speaker",
"members": [
{ "productId": 412, "variantLabel": "White" },
{ "productId": 413, "variantLabel": "Black" }
]
}
Response (201 - Created)
The response carries a Location header that points at the new group.
{
"success": true,
"data": {
"id": 12,
"make": "Biamp",
"name": "C-IC6LP",
"optionName": "Colour",
"description": "6.5-inch low-profile ceiling speaker",
"members": [
{
"productId": 413,
"model": "C-IC6LP-B",
"part": "C-IC6LP-B",
"variantLabel": "Black",
"catalogPrice": 205.00,
"vendor": "ADI Global",
"productType": "Ceiling Speaker",
"hasMetadata": false
},
{
"productId": 412,
"model": "C-IC6LP-W",
"part": "C-IC6LP-W",
"variantLabel": "White",
"catalogPrice": 212.00,
"vendor": "ADI Global",
"productType": "Ceiling Speaker",
"hasMetadata": true
}
]
},
"message": "Variant group created",
"timestamp": "2026-10-08T15:30:00Z"
}
Error Responses
| Status | Code | Description |
| 400 | VARIANT_GROUP_RULE | A rule was broken: fewer than two products, more than one make, a product that is already in a group, a product ID that was not found, a product listed twice, a missing or too long value, or a name the make already uses |
| 400 | UNKNOWN_FIELDS | The body names a field other than name, optionName, description and members |
| 403 | READ_ONLY_ACCESS | The caller has read-only access |
PUT /api/v1/variant-groups/{id}
Renames a variant group, changes its option name, or changes its description. Send only the fields to change. A field that is left out keeps its value. The make and the members cannot be changed with this endpoint.
Path Parameters
| Field |
Type |
Required |
Description |
id |
integer |
Yes |
The variant group's ID |
Request Body
| Field |
Type |
Required |
Description |
name |
string |
No |
The new group name, up to 100 characters. It must be unique within the make. |
optionName |
string |
No |
The new option name, up to 50 characters |
description |
string |
No |
The new description, up to 500 characters. An empty string clears it. |
Example Request
{
"optionName": "Finish"
}
Response (200 - Success)
{
"success": true,
"data": {
"id": 12,
"make": "Biamp",
"name": "C-IC6LP",
"optionName": "Finish",
"description": "6.5-inch low-profile ceiling speaker",
"members": [
{
"productId": 413,
"model": "C-IC6LP-B",
"part": "C-IC6LP-B",
"variantLabel": "Black",
"catalogPrice": 205.00,
"vendor": "ADI Global",
"productType": "Ceiling Speaker",
"hasMetadata": false
},
{
"productId": 412,
"model": "C-IC6LP-W",
"part": "C-IC6LP-W",
"variantLabel": "White",
"catalogPrice": 212.00,
"vendor": "ADI Global",
"productType": "Ceiling Speaker",
"hasMetadata": true
}
]
},
"message": "Variant group updated",
"timestamp": "2026-10-08T15:30:00Z"
}
Error Responses
| Status | Code | Description |
| 400 | UNKNOWN_FIELDS | The body names a field other than name, optionName and description |
| 400 | NO_CHANGES | The body names none of the three fields |
| 400 | VARIANT_GROUP_RULE | A rule was broken: an empty name or optionName, a value that is too long, or a name the make already uses for another group |
| 403 | READ_ONLY_ACCESS | The caller has read-only access |
| 404 | VARIANT_GROUP_NOT_FOUND | No variant group has this ID |
DELETE /api/v1/variant-groups/{id}
Deletes a variant group. Its products stay in the catalog and lose their group and their labels.
Path Parameters
| Field |
Type |
Required |
Description |
id |
integer |
Yes |
The variant group's ID |
Example Request
DELETE /api/v1/variant-groups/12
Authorization: Bearer YOUR_JWT_TOKEN
Response (200 - Success)
{
"success": true,
"data": {
"id": 12,
"deleted": true
},
"message": "Variant group deleted; its products are ungrouped",
"timestamp": "2026-10-08T15:30:00Z"
}
Error Responses
| Status | Code | Description |
| 403 | READ_ONLY_ACCESS | The caller has read-only access |
| 404 | VARIANT_GROUP_NOT_FOUND | No variant group has this ID |
POST /api/v1/variant-groups/{id}/members
Adds products to a variant group. Each product must be of the group's make and must not already be in a group.
Path Parameters
| Field |
Type |
Required |
Description |
id |
integer |
Yes |
The variant group's ID |
Request Body
| Field |
Type |
Required |
Description |
members |
array |
Yes |
The products to add. The list must name at least one product. |
members[].productId |
integer |
Yes |
A catalog product ID. A product may be listed once. |
members[].variantLabel |
string |
Yes |
The product's value for the option, up to 50 characters |
Example Request
{
"members": [
{ "productId": 414, "variantLabel": "Silver" }
]
}
Response (200 - Success)
{
"success": true,
"data": {
"id": 12,
"make": "Biamp",
"name": "C-IC6LP",
"optionName": "Colour",
"description": "6.5-inch low-profile ceiling speaker",
"members": [
{
"productId": 413,
"model": "C-IC6LP-B",
"part": "C-IC6LP-B",
"variantLabel": "Black",
"catalogPrice": 205.00,
"vendor": "ADI Global",
"productType": "Ceiling Speaker",
"hasMetadata": false
},
{
"productId": 414,
"model": "C-IC6LP-S",
"part": "C-IC6LP-S",
"variantLabel": "Silver",
"catalogPrice": 219.00,
"vendor": "ADI Global",
"productType": "Ceiling Speaker",
"hasMetadata": false
},
{
"productId": 412,
"model": "C-IC6LP-W",
"part": "C-IC6LP-W",
"variantLabel": "White",
"catalogPrice": 212.00,
"vendor": "ADI Global",
"productType": "Ceiling Speaker",
"hasMetadata": true
}
]
},
"message": "Products added to the variant group",
"timestamp": "2026-10-08T15:30:00Z"
}
Error Responses
| Status | Code | Description |
| 400 | VARIANT_GROUP_RULE | A rule was broken: an empty list, a product of another make, a product that is already in a group, a product ID that was not found, a product listed twice, or a missing or too long label |
| 403 | READ_ONLY_ACCESS | The caller has read-only access |
| 404 | VARIANT_GROUP_NOT_FOUND | No variant group has this ID |
PUT /api/v1/variant-groups/{id}/members/{productId}
Changes the label of one member of a variant group.
Path Parameters
| Field |
Type |
Required |
Description |
id |
integer |
Yes |
The variant group's ID |
productId |
integer |
Yes |
The catalog product ID of the member |
Request Body
| Field |
Type |
Required |
Description |
variantLabel |
string |
Yes |
The member's new label, up to 50 characters |
Example Request
{
"variantLabel": "Arctic White"
}
Response (200 - Success)
{
"success": true,
"data": {
"id": 12,
"make": "Biamp",
"name": "C-IC6LP",
"optionName": "Colour",
"description": "6.5-inch low-profile ceiling speaker",
"members": [
{
"productId": 413,
"model": "C-IC6LP-B",
"part": "C-IC6LP-B",
"variantLabel": "Black",
"catalogPrice": 205.00,
"vendor": "ADI Global",
"productType": "Ceiling Speaker",
"hasMetadata": false
},
{
"productId": 414,
"model": "C-IC6LP-S",
"part": "C-IC6LP-S",
"variantLabel": "Silver",
"catalogPrice": 219.00,
"vendor": "ADI Global",
"productType": "Ceiling Speaker",
"hasMetadata": false
},
{
"productId": 412,
"model": "C-IC6LP-W",
"part": "C-IC6LP-W",
"variantLabel": "Arctic White",
"catalogPrice": 212.00,
"vendor": "ADI Global",
"productType": "Ceiling Speaker",
"hasMetadata": true
}
]
},
"message": "Variant label updated",
"timestamp": "2026-10-08T15:30:00Z"
}
Error Responses
| Status | Code | Description |
| 400 | VARIANT_GROUP_RULE | The label is empty or longer than 50 characters |
| 403 | READ_ONLY_ACCESS | The caller has read-only access |
| 404 | VARIANT_GROUP_MEMBER_NOT_FOUND | The product is not a member of this group, or no variant group has this ID |
DELETE /api/v1/variant-groups/{id}/members/{productId}
Takes one product out of a variant group and clears its label. The product stays in the catalog. Removing the last member deletes the group.
Path Parameters
| Field |
Type |
Required |
Description |
id |
integer |
Yes |
The variant group's ID |
productId |
integer |
Yes |
The catalog product ID of the member |
Example Request
DELETE /api/v1/variant-groups/12/members/414
Authorization: Bearer YOUR_JWT_TOKEN
Response (200 - Success)
{
"success": true,
"data": {
"groupId": 12,
"productId": 414,
"removed": true
},
"message": "Product removed from the variant group",
"timestamp": "2026-10-08T15:30:00Z"
}
Error Responses
| Status | Code | Description |
| 403 | READ_ONLY_ACCESS | The caller has read-only access |
| 404 | VARIANT_GROUP_MEMBER_NOT_FOUND | The product is not a member of this group, or no variant group has this ID |
POST /api/v1/variant-groups/suggest
Suggests a variant group for a set of products: a group name, an option name and one label for each product. The endpoint writes nothing, so a caller with read-only access can use it. Use the result to fill a create request.
Request Body
| Field |
Type |
Required |
Description |
productIds |
array of integer |
Yes |
The catalog product IDs to group. Send two or more. |
Example Request
{
"productIds": [412, 413]
}
Response (200 - Success)
{
"success": true,
"data": {
"name": "C-IC6LP",
"optionName": "Colour",
"members": [
{
"productId": 412,
"make": "Biamp",
"model": "C-IC6LP-W",
"variantLabel": "White"
},
{
"productId": 413,
"make": "Biamp",
"model": "C-IC6LP-B",
"variantLabel": "Black"
}
],
"problems": []
},
"message": null,
"timestamp": "2026-10-08T15:30:00Z"
}
| Field |
Type |
Description |
name |
string |
The start of the model numbers that all the products share. It is an empty string when they share none. |
optionName |
string |
The suggested option name. It is always Colour. |
members |
array |
One entry for each product that was found, in the order of the request. Each entry holds the product's productId, make and model, and a suggested variantLabel. |
problems |
array of string |
Each reason these products cannot be grouped as they are: fewer than two products, a product ID that was not found, more than one make, or a product that is already in a group. The list is empty when none of these applies. |
A set of products that cannot be grouped still returns 200. Read problems to find out. The name is yours to change, so problems does not report a name that the make already uses. The create endpoint reports that.
Errors
The two not-found codes, UNKNOWN_FIELDS and NO_CHANGES use the standard error body. See Error Handling.
{
"success": false,
"error": {
"code": "VARIANT_GROUP_NOT_FOUND",
"message": "Variant group with ID 12 not found",
"details": null
},
"timestamp": "2026-10-08T15:30:00Z"
}
400 VARIANT_GROUP_RULE
A broken rule returns a different body. error.problems is an array with one sentence for each broken rule. error.message is the same sentences joined with ; . This body has no details field and no timestamp.
{
"success": false,
"error": {
"code": "VARIANT_GROUP_RULE",
"message": "Enter a group name.; Product 999 was not found.",
"problems": [
"Enter a group name.",
"Product 999 was not found."
]
}
}
The same code and body are returned by PUT /api/v1/products/{id} when it would change the make of a product that is in a group, and by POST /api/v1/products/{id}/metadata/apply-to-variants when the source product has no connection metadata.
| Status | Code | Description |
| 400 | VARIANT_GROUP_RULE | A rule was broken and nothing was written. error.problems lists every broken rule. |
| 400 | UNKNOWN_FIELDS | The body of a create or an update names a field the endpoint does not accept. The message lists those fields and the fields that are valid. |
| 400 | NO_CHANGES | An update named none of name, optionName and description |
| 403 | READ_ONLY_ACCESS | The caller has read-only access. Every endpoint on this page returns it except the two GET endpoints and suggest. |
| 404 | VARIANT_GROUP_NOT_FOUND | No variant group has this ID. Returned by get, update, delete and add members. |
| 404 | VARIANT_GROUP_MEMBER_NOT_FOUND | The product is not a member of the group, or the group does not exist. Returned by the two /members/{productId} endpoints. |
A null is not the same as a missing field
On the endpoints that write, a required field that is left out is reported as a VARIANT_GROUP_RULE problem, for example Enter a group name. A JSON null in place of a required field is refused earlier, by request validation. So is a member entry that has no variantLabel. That 400 response uses the framework's validation format, with title, status and errors. It does not use the error bodies shown above.