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 a read scope: a key carrying either catalog:read or commerce:read is accepted.
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 a read scope: a key carrying either catalog:read or commerce:read is accepted.
POST /v1/api/commerce/configurator/products/{id}/quote
Prices a specific configuration of an active made-to-measure product (capability made_to_measure). The server validates the selected choices (existence, availability, at most one per single group, required groups covered, requires and excludes rules), then the dimensions (every configured dimension present, a whole number, 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 with capability made_to_measure. |
Body parameters
The request body is JSON.
| Name | Type | Required | Description |
|---|---|---|---|
| dimensions | object | Conditional | Map of dimension key (from the schema) to a whole-number value in the dimension's unit (for example cm). A fractional value is refused with a 422 on dimensions.{key}. 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 available choice of this product, that is, one the schema endpoint publishes. 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 carries neither the catalog:read nor the commerce:read scope. |
| 403 | {"error":"Module not enabled: configurator"} | Tenant does not have the configurator 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 its capability is not made_to_measure. |
| 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>", ...]}}, with no top-level code. This differs from a cart add, which reports the same configuration errors with a top-level code of INVALID_CONFIGURATION (see Cart lines). 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 not available: inactive, deleted, or left without a satisfiable requires target group. The message is deliberately generic and never names the unavailable choice. |
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). {names} lists only choices the schema publishes for this product, so a name in this message is always one the buyer can select. |
dimensions.{key} | {Label} is required. | The dimension is missing from the request or its value is not numeric. |
dimensions.{key} | {Label} must be a whole number. | The value has a fractional part, for example 120.5. |
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. A matrix conditioned on a choice that is not available can never be satisfied, so it never supplies a base price and the schema does not list it either. |
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 made-to-measure product (capability made_to_measure). The server validates the selected choices (existence, availability, at most one per single group, required groups covered, requires and excludes rules), then the dimensions (every configured dimension present, a whole number, 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 with capability made_to_measure. |
Body parameters
The request body is JSON.
| Name | Type | Required | Description |
|---|---|---|---|
| dimensions | object | Conditional | Map of dimension key (from the schema) to a whole-number value in the dimension's unit (for example cm). A fractional value is refused with a 422 on dimensions.{key}. 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 available choice of this product, that is, one the schema endpoint publishes. 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 carries neither the catalog:read nor the commerce:read scope. |
| 403 | {"error":"Module not enabled: configurator"} | Tenant does not have the configurator 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 its capability is not made_to_measure. |
| 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>", ...]}}, with no top-level code. This differs from a cart add, which reports the same configuration errors with a top-level code of INVALID_CONFIGURATION (see Cart lines). 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 not available: inactive, deleted, or left without a satisfiable requires target group. The message is deliberately generic and never names the unavailable choice. |
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). {names} lists only choices the schema publishes for this product, so a name in this message is always one the buyer can select. |
dimensions.{key} | {Label} is required. | The dimension is missing from the request or its value is not numeric. |
dimensions.{key} | {Label} must be a whole number. | The value has a fractional part, for example 120.5. |
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. A matrix conditioned on a choice that is not available can never be satisfied, so it never supplies a base price and the schema does not list it either. |
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.