Configurator
Price quote
Price a specific configuration of a configurable product and get a line-item breakdown.
View as MarkdownThe quote endpoint prices one concrete configuration. Call it on every change to the buyer's selection to show a live total. It is read-only: no cart or order is created, and it requires only the commerce:read scope.
The quote endpoint prices one concrete configuration. Call it on every change to the buyer's selection to show a live total. It is read-only: no cart or order is created, and it requires only the commerce:read scope.
POST /v1/api/commerce/configurator/products/{id}/quote
Prices a specific configuration of an active configurable product. The server validates the selected choices (existence, active, at most one per single group, required groups covered, requires and excludes rules), then the dimensions (every configured dimension present, integer, inside its min and max after tightening by the selected choices' constraints), resolves the base price from the winning price matrix (or the product price when the product has no dimensions), applies choice deltas in two passes (fixed, per_area, and percent first, then each percent_of_total against that subtotal without compounding), and returns the total with a line-item breakdown. See the chapter introduction for the full algorithm.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | string (uuid) | Yes | Product ID. Must be an active product of type configurable. |
Body parameters
The request body is JSON.
| Name | Type | Required | Description |
|---|---|---|---|
| dimensions | object | Conditional | Map of dimension key (from the schema) to integer value in the dimension's unit (for example cm). Required whenever the product has dimensions: every configured dimension must be present and numeric. Unknown keys are rejected with a 422. May be omitted only for a product with no dimensions. |
| choices | array of string (uuid) | No | Selected option choice IDs. Duplicates are de-duplicated. Each element must be a UUID referencing an active choice of this product. Required groups must be covered, and at most one choice per single group is allowed. Omit or send an empty array only when the product has no required groups. |
Response fields
| Name | Type | Required | Description |
|---|---|---|---|
| total | integer | always | Total price in minor units (cents). |
| currency | string | always | The product's ISO currency code, for example "EUR". |
| breakdown | array | always | Line items. Always starts with {"label":"Base","amount":<base>}, followed by one line per priced choice labeled "{Group name}: {Choice name}", in group and choice position order (with all percent_of_total lines after the others). Choices with a delta of 0 still appear with amount: 0. All amounts are integer minor units. |
| matrix | object or null | always | {id, name} of the price matrix that supplied the base price. null when the base came from the product's own price (a product without dimensions). |
Errors
Authentication and infrastructure errors are the same as on the other storefront routes:
| Status | Body | Cause |
|---|---|---|
| 401 | {"message":"Unauthenticated."} | Missing or invalid API key. |
| 403 | {"message":"Invalid ability provided."} | Key lacks the commerce:read scope. |
| 403 | {"error":"Module not enabled: commerce"} | Tenant does not have the commerce module enabled. Note the error key instead of message. |
| 403 | {"message":"Tenant suspended.","code":"tenant_suspended"} | Tenant account is suspended. |
| 404 | {"message":"..."} | Product does not exist, is not active, or is not of type configurable. |
| 429 | {"message":"Too Many Attempts."} | Rate limit of 120 requests per minute exceeded. |
422 validation errors
All validation failures use the standard Laravel envelope: {"message":"<first error>","errors":{"<field>":["<message>", ...]}}. There are two layers.
Input validation (shape of the request body): dimensions must be an object, choices must be an array, and every choices.* element must be a UUID. Example:
Domain validation (the configuration itself): the same envelope; each error key carries exactly one message. Choice errors are checked first and short-circuit: when any choice error occurs, dimensions are not validated in that response. The possible keys and messages:
| Error key | Message | Cause |
|---|---|---|
choices | One or more choices are not available. | A choice ID does not exist on this product, or the choice is inactive or deleted. |
choices | Choose one {Group}. | Two or more choices selected in a single type group. |
choices | {Group} is required. | A required group has no selected choice. |
choices | Area-priced choice needs two dimensions. | A per_area choice was selected on a product that does not have exactly two dimensions. |
choices.{choiceId} | {Choice} cannot be combined with {Target}. | An excludes rule is violated. |
choices.{choiceId} | {Choice} requires one of: {names}. | A requires rule is unmet (none of the targets in one target group is selected). |
dimensions.{key} | {Label} is required. | The dimension is missing from the request or its value is not numeric. |
dimensions.{key} | {Label} must be between {min} and {max} {unit}. | The value is outside the dimension's range. |
dimensions.{key} | {Label} must be between {min} and {max} {unit} when {choice names} is selected. | The value is outside the range after tightening by the selected choices' constraints. |
dimensions.{key} | {Label} has no valid range for the selected choices ({names}). | The selected choices' constraints collapse the range (effective min exceeds effective max). |
dimensions.{key} | Unknown dimension. | The request contains a dimension key that is not configured on the product. |
configuration | not_priceable | No base price could be resolved: a product without dimensions has a null price, no price matrix's conditions are satisfied by the selected choices, or the winning matrix has no price rule covering the dimension values. |
Example domain validation failure:
Note that requires_quote on a choice never causes an error here: the endpoint prices such configurations normally. The flag, exposed in the schema, only signals that the storefront should route the buyer to a manual quote request instead of direct checkout.
curl -X POST "https://api.borkol.com/v1/api/commerce/configurator/products/9c2f1e6a-8b4d-4f2a-9e11-3d5a7b9c0e21/quote" \
-H "Authorization: Bearer {api_key}" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"dimensions": { "width": 420, "depth": 300 },
"choices": [
"c1a2b3c4-d5e6-4f7a-8b9c-0d1e2f3a4b5c",
"f6a7b8c9-d0e1-4f2a-b3c4-d5e6f7a8b9c0"
]
}'const res = await fetch(
`https://api.borkol.com/v1/api/commerce/configurator/products/${productId}/quote`,
{
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
dimensions: { width: 420, depth: 300 },
choices: [
"c1a2b3c4-d5e6-4f7a-8b9c-0d1e2f3a4b5c",
"f6a7b8c9-d0e1-4f2a-b3c4-d5e6f7a8b9c0",
],
}),
}
);
if (res.status === 422) {
const { errors } = await res.json();
// errors is a map of field => [message]; render per-field.
} else {
const { data } = await res.json();
}{
"data": {
"total": 561500,
"currency": "EUR",
"breakdown": [
{ "label": "Base", "amount": 530000 },
{ "label": "LED lighting: LED set (6)", "amount": 31500 }
],
"matrix": {
"id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f7a",
"name": "Glass roof"
}
}
}{
"message": "The choices.0 field must be a valid UUID.",
"errors": {
"choices.0": ["The choices.0 field must be a valid UUID."]
}
}{
"message": "Width must be between 200 and 600 cm when Glass is selected.",
"errors": {
"dimensions.width": [
"Width must be between 200 and 600 cm when Glass is selected."
]
}
}POST /v1/api/commerce/configurator/products/{id}/quote
Prices a specific configuration of an active configurable product. The server validates the selected choices (existence, active, at most one per single group, required groups covered, requires and excludes rules), then the dimensions (every configured dimension present, integer, inside its min and max after tightening by the selected choices' constraints), resolves the base price from the winning price matrix (or the product price when the product has no dimensions), applies choice deltas in two passes (fixed, per_area, and percent first, then each percent_of_total against that subtotal without compounding), and returns the total with a line-item breakdown. See the chapter introduction for the full algorithm.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | string (uuid) | Yes | Product ID. Must be an active product of type configurable. |
Body parameters
The request body is JSON.
| Name | Type | Required | Description |
|---|---|---|---|
| dimensions | object | Conditional | Map of dimension key (from the schema) to integer value in the dimension's unit (for example cm). Required whenever the product has dimensions: every configured dimension must be present and numeric. Unknown keys are rejected with a 422. May be omitted only for a product with no dimensions. |
| choices | array of string (uuid) | No | Selected option choice IDs. Duplicates are de-duplicated. Each element must be a UUID referencing an active choice of this product. Required groups must be covered, and at most one choice per single group is allowed. Omit or send an empty array only when the product has no required groups. |
curl -X POST "https://api.borkol.com/v1/api/commerce/configurator/products/9c2f1e6a-8b4d-4f2a-9e11-3d5a7b9c0e21/quote" \
-H "Authorization: Bearer {api_key}" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"dimensions": { "width": 420, "depth": 300 },
"choices": [
"c1a2b3c4-d5e6-4f7a-8b9c-0d1e2f3a4b5c",
"f6a7b8c9-d0e1-4f2a-b3c4-d5e6f7a8b9c0"
]
}'const res = await fetch(
`https://api.borkol.com/v1/api/commerce/configurator/products/${productId}/quote`,
{
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
dimensions: { width: 420, depth: 300 },
choices: [
"c1a2b3c4-d5e6-4f7a-8b9c-0d1e2f3a4b5c",
"f6a7b8c9-d0e1-4f2a-b3c4-d5e6f7a8b9c0",
],
}),
}
);
if (res.status === 422) {
const { errors } = await res.json();
// errors is a map of field => [message]; render per-field.
} else {
const { data } = await res.json();
}{
"data": {
"total": 561500,
"currency": "EUR",
"breakdown": [
{ "label": "Base", "amount": 530000 },
{ "label": "LED lighting: LED set (6)", "amount": 31500 }
],
"matrix": {
"id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f7a",
"name": "Glass roof"
}
}
}Response fields
| Name | Type | Required | Description |
|---|---|---|---|
| total | integer | always | Total price in minor units (cents). |
| currency | string | always | The product's ISO currency code, for example "EUR". |
| breakdown | array | always | Line items. Always starts with {"label":"Base","amount":<base>}, followed by one line per priced choice labeled "{Group name}: {Choice name}", in group and choice position order (with all percent_of_total lines after the others). Choices with a delta of 0 still appear with amount: 0. All amounts are integer minor units. |
| matrix | object or null | always | {id, name} of the price matrix that supplied the base price. null when the base came from the product's own price (a product without dimensions). |
Errors
Authentication and infrastructure errors are the same as on the other storefront routes:
| Status | Body | Cause |
|---|---|---|
| 401 | {"message":"Unauthenticated."} | Missing or invalid API key. |
| 403 | {"message":"Invalid ability provided."} | Key lacks the commerce:read scope. |
| 403 | {"error":"Module not enabled: commerce"} | Tenant does not have the commerce module enabled. Note the error key instead of message. |
| 403 | {"message":"Tenant suspended.","code":"tenant_suspended"} | Tenant account is suspended. |
| 404 | {"message":"..."} | Product does not exist, is not active, or is not of type configurable. |
| 429 | {"message":"Too Many Attempts."} | Rate limit of 120 requests per minute exceeded. |
422 validation errors
All validation failures use the standard Laravel envelope: {"message":"<first error>","errors":{"<field>":["<message>", ...]}}. There are two layers.
Input validation (shape of the request body): dimensions must be an object, choices must be an array, and every choices.* element must be a UUID. Example:
{
"message": "The choices.0 field must be a valid UUID.",
"errors": {
"choices.0": ["The choices.0 field must be a valid UUID."]
}
}Domain validation (the configuration itself): the same envelope; each error key carries exactly one message. Choice errors are checked first and short-circuit: when any choice error occurs, dimensions are not validated in that response. The possible keys and messages:
| Error key | Message | Cause |
|---|---|---|
choices | One or more choices are not available. | A choice ID does not exist on this product, or the choice is inactive or deleted. |
choices | Choose one {Group}. | Two or more choices selected in a single type group. |
choices | {Group} is required. | A required group has no selected choice. |
choices | Area-priced choice needs two dimensions. | A per_area choice was selected on a product that does not have exactly two dimensions. |
choices.{choiceId} | {Choice} cannot be combined with {Target}. | An excludes rule is violated. |
choices.{choiceId} | {Choice} requires one of: {names}. | A requires rule is unmet (none of the targets in one target group is selected). |
dimensions.{key} | {Label} is required. | The dimension is missing from the request or its value is not numeric. |
dimensions.{key} | {Label} must be between {min} and {max} {unit}. | The value is outside the dimension's range. |
dimensions.{key} | {Label} must be between {min} and {max} {unit} when {choice names} is selected. | The value is outside the range after tightening by the selected choices' constraints. |
dimensions.{key} | {Label} has no valid range for the selected choices ({names}). | The selected choices' constraints collapse the range (effective min exceeds effective max). |
dimensions.{key} | Unknown dimension. | The request contains a dimension key that is not configured on the product. |
configuration | not_priceable | No base price could be resolved: a product without dimensions has a null price, no price matrix's conditions are satisfied by the selected choices, or the winning matrix has no price rule covering the dimension values. |
Example domain validation failure:
{
"message": "Width must be between 200 and 600 cm when Glass is selected.",
"errors": {
"dimensions.width": [
"Width must be between 200 and 600 cm when Glass is selected."
]
}
}Note that requires_quote on a choice never causes an error here: the endpoint prices such configurations normally. The flag, exposed in the schema, only signals that the storefront should route the buyer to a manual quote request instead of direct checkout.