Carts & Checkout
Cart lines
Add products to a cart, change line quantities, and remove lines, including variant, made-to-measure and decimal quantity rules.
View as MarkdownCart lines hold the products in a cart. Each line snapshots the product name and SKU at add time, stores the resolved unit price in minor units, and, for made-to-measure products, a full configuration snapshot. All three endpoints require the commerce:write scope, an active cart, and pass the cart ownership check (see the Carts page). Every endpoint returns the full cart envelope, the same shape as GET /v1/api/commerce/carts/{id}.
Cart lines hold the products in a cart. Each line snapshots the product name and SKU at add time, stores the resolved unit price in minor units, and, for made-to-measure products, a full configuration snapshot. All three endpoints require the commerce:write scope, an active cart, and pass the cart ownership check (see the Carts page). Every endpoint returns the full cart envelope, the same shape as GET /v1/api/commerce/carts/{id}.
Product capability rules
How a product is added depends on its capability (see Products):
standardproducts with active variants:variant_idis required and must be an active variant of the product.standardproducts without variants:variant_idmust be omitted.made_to_measureproducts:configurationis required andvariant_idis forbidden. The configuration is an object{"dimensions": {...}, "choices": ["choice_id", ...]}priced by the configurator engine; dimensions are whole numbers. The line stores aconfig_hashplus a full snapshot underattributes.configuration(dimensions, choices, price breakdown, component list, matrix cell). Component stock is reserved per configurator component.- For
standardproducts,configurationis forbidden. - A product whose
capability_activeisfalsecannot be added: the commerce module sellsstandardproducts and the configurator module sellsmade_to_measureones.
Product capability rules
How a product is added depends on its capability (see Products):
standardproducts with active variants:variant_idis required and must be an active variant of the product.standardproducts without variants:variant_idmust be omitted.made_to_measureproducts:configurationis required andvariant_idis forbidden. The configuration is an object{"dimensions": {...}, "choices": ["choice_id", ...]}priced by the configurator engine; dimensions are whole numbers. The line stores aconfig_hashplus a full snapshot underattributes.configuration(dimensions, choices, price breakdown, component list, matrix cell). Component stock is reserved per configurator component.- For
standardproducts,configurationis forbidden. - A product whose
capability_activeisfalsecannot be added: the commerce module sellsstandardproducts and the configurator module sellsmade_to_measureones.
Quantities
quantity is a JSON number greater than zero with at most 8 integer digits and at most 3 decimals (maximum 99999999.999). Send it as a JSON number or as a plain decimal string with a dot ("0.75"); a string with a decimal comma or an exponent ("0,75", "1e3") is refused. A product with unit piece takes whole numbers only (2 or 2.0, never 2.5); a product in any other unit (kg, m2, hour, ...) takes up to three decimals, for example 0.75. Quantities come back as JSON numbers (0.75, 12). A line total is the unit price times the quantity, rounded half up to whole minor units.
Quantities
quantity is a JSON number greater than zero with at most 8 integer digits and at most 3 decimals (maximum 99999999.999). Send it as a JSON number or as a plain decimal string with a dot ("0.75"); a string with a decimal comma or an exponent ("0,75", "1e3") is refused. A product with unit piece takes whole numbers only (2 or 2.0, never 2.5); a product in any other unit (kg, m2, hour, ...) takes up to three decimals, for example 0.75. Quantities come back as JSON numbers (0.75, 12). A line total is the unit price times the quantity, rounded half up to whole minor units.
Refusal codes
When a line cannot be added, the 422 body carries a machine-readable top-level code next to message and errors:
code | Meaning |
|---|---|
CAPABILITY_INACTIVE | The product's capability cannot be sold right now (capability_active is false): the module that sells it is disabled for the store. Reported on errors.product_id. |
INVALID_CONFIGURATION | The variant or configuration does not fit the product: a variant is missing, unknown, or not allowed; a configuration is missing or sent for a standard product; or the configurator engine refused the configuration (its error keys, for example dimensions.width, are passed through). |
NOT_PURCHASABLE | The product cannot be sold, for example it has no resolvable price. Reported on errors.product_id. |
Plain input validation (a missing or malformed product_id or quantity, an unknown or inactive product, a fractional quantity for a piece product) uses the standard 422 envelope without code. Branch on code when present, and fall back to errors otherwise.
Lines deduplicate on the combination of product, variant, and configuration hash: adding the same combination again merges into the existing line, summing the quantity and re-resolving the unit price at the new quantity tier.
Unit prices resolve against the request's pricing context: the customer identified by X-Customer-Token if present, else the cart's bound customer, else guest pricing.
{
"message": "Width must be a whole number.",
"errors": {
"dimensions.width": ["Width must be a whole number."]
},
"code": "INVALID_CONFIGURATION"
}Refusal codes
When a line cannot be added, the 422 body carries a machine-readable top-level code next to message and errors:
code | Meaning |
|---|---|
CAPABILITY_INACTIVE | The product's capability cannot be sold right now (capability_active is false): the module that sells it is disabled for the store. Reported on errors.product_id. |
INVALID_CONFIGURATION | The variant or configuration does not fit the product: a variant is missing, unknown, or not allowed; a configuration is missing or sent for a standard product; or the configurator engine refused the configuration (its error keys, for example dimensions.width, are passed through). |
NOT_PURCHASABLE | The product cannot be sold, for example it has no resolvable price. Reported on errors.product_id. |
{
"message": "Width must be a whole number.",
"errors": {
"dimensions.width": ["Width must be a whole number."]
},
"code": "INVALID_CONFIGURATION"
}Plain input validation (a missing or malformed product_id or quantity, an unknown or inactive product, a fractional quantity for a piece product) uses the standard 422 envelope without code. Branch on code when present, and fall back to errors otherwise.
Lines deduplicate on the combination of product, variant, and configuration hash: adding the same combination again merges into the existing line, summing the quantity and re-resolving the unit price at the new quantity tier.
Unit prices resolve against the request's pricing context: the customer identified by X-Customer-Token if present, else the cart's bound customer, else guest pricing.
POST /v1/api/commerce/carts/{id}/lines
Adds a product to the cart, or merges into an existing identical line. Returns the full cart with status 201.
Parameters
Path
| Name | Type | Required | Description |
|---|---|---|---|
id | string (UUID) | yes | Cart id. Must be an active cart. |
Body
| Name | Type | Required | Description |
|---|---|---|---|
product_id | string (UUID) | yes | The product to add. Must be an active product of the store. |
variant_id | string (UUID) or null | conditional | Required when the product has active variants; forbidden otherwise, and always forbidden for made_to_measure products. |
quantity | number | yes | Greater than zero, at most 3 decimals; a whole number for a piece product (see Quantities above). The merged line quantity may not exceed the policy's max_quantity_per_line. |
configuration | object | conditional | Required for made_to_measure products, forbidden otherwise. Shape: {"dimensions": {...}, "choices": [choice_ids]}, dimensions as whole numbers. Validated by the configurator engine. |
Headers
| Name | Type | Required | Description |
|---|---|---|---|
X-Customer-Token | string | no | Selects the pricing context; required if the cart is bound to a non-guest customer. |
For a made_to_measure product, the created line carries the configuration snapshot. components[].qty is an exact decimal string per configured unit (multiply by the line quantity for the stock reserved):
Errors
Line refusals are 422 errors with the message attached to the offending field in errors and a top-level code (see Refusal codes above):
| Status | Body | When |
|---|---|---|
| 422 | errors.product_id: ["Product is not available."] | Product unknown or not active. No code. |
| 422 | errors.product_id: ["Product is not available."], code: "NOT_PURCHASABLE" | The product exists and is active but cannot be sold, for example it has no resolvable price. |
| 422 | errors.product_id: ["Product is not available."], code: "CAPABILITY_INACTIVE" | The product's capability is not active for the store (capability_active is false). |
| 422 | errors.variant_id: ["Choose a variant for this product."], code: "INVALID_CONFIGURATION" | Product has active variants and variant_id was omitted. |
| 422 | errors.variant_id: ["Variant is not available."], code: "INVALID_CONFIGURATION" | Variant id does not resolve to an active variant of the product. |
| 422 | errors.variant_id: ["This product has no variants."], code: "INVALID_CONFIGURATION" | variant_id sent for a product without variants. |
| 422 | errors.variant_id: ["Configurable products have no variants."], code: "INVALID_CONFIGURATION" | variant_id sent for a made_to_measure product. |
| 422 | errors.configuration: ["Configure this product before adding it."], code: "INVALID_CONFIGURATION" | made_to_measure product without a configuration. |
| 422 | errors.configuration: ["This product is not configurable."], code: "INVALID_CONFIGURATION" | configuration sent for a standard product. |
| 422 | errors.choices, errors.choices.{choiceId}, errors.dimensions.{key}, errors.configuration, code: "INVALID_CONFIGURATION" | Configurator engine errors, attached to their own field keys exactly as documented in the quote error catalog, including {Label} must be a whole number. for a fractional dimension. |
| 422 | errors.quantity: ["The quantity must be a whole number."] | Fractional quantity for a piece product. No code. |
| 422 | errors.quantity: ["..."] | Quantity missing, not a plain decimal (comma, exponent, more than 3 decimals or 8 integer digits), or not greater than zero. No code. |
| 422 | {"message": "Quantity exceeds the per-line maximum.", "code": "cart_constraint"} | Merged quantity would exceed max_quantity_per_line. |
| 422 | {"message": "Cart line limit reached.", "code": "cart_constraint"} | Adding a new distinct line would exceed max_lines. |
| 404 | {"message": "..."} | Cart unknown, expired, or not active. |
| 403 | ownership, scope, module, or suspension body | See the Carts page. |
| 429 | {"message": "Too Many Attempts."} | Rate limit exceeded. |
curl -X POST https://api.borkol.com/v1/api/commerce/carts/9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4/lines \
-H "Authorization: Bearer {api_key}" \
-H "Content-Type: application/json" \
-d '{
"product_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"variant_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
"quantity": 2
}'const res = await fetch(
`https://api.borkol.com/v1/api/commerce/carts/${cartId}/lines`,
{
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
product_id: "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
variant_id: "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
quantity: 2,
}),
}
);
const { data: cart } = await res.json();{
"data": {
"id": "9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4",
"status": "active",
"lines": [
{
"id": "5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9",
"product_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"variant_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
"name": "Garden Chair",
"sku": "CHAIR-GRN-M",
"quantity": 2,
"unit_price_amount": 2250,
"unit_price": { "amount": 2250, "currency": "EUR" },
"line_total": { "amount": 4500, "currency": "EUR" },
"attributes": null
}
],
"...": "same cart shape as GET /v1/api/commerce/carts/{id}"
}
}{
"attributes": {
"configuration": {
"dimensions": { "width": 120, "depth": 180 },
"choices": ["c1d2e3f4-..."],
"breakdown": [
{ "label": "Base", "amount": 18900 },
{ "label": "Motor: Motorised", "amount": 7500 }
],
"components": [{ "product_id": "...", "qty": "2.16" }],
"matrix": { "id": "...", "name": "Fabric blinds" }
}
}
}POST /v1/api/commerce/carts/{id}/lines
Adds a product to the cart, or merges into an existing identical line. Returns the full cart with status 201.
Parameters
Path
| Name | Type | Required | Description |
|---|---|---|---|
id | string (UUID) | yes | Cart id. Must be an active cart. |
Body
| Name | Type | Required | Description |
|---|---|---|---|
product_id | string (UUID) | yes | The product to add. Must be an active product of the store. |
variant_id | string (UUID) or null | conditional | Required when the product has active variants; forbidden otherwise, and always forbidden for made_to_measure products. |
quantity | number | yes | Greater than zero, at most 3 decimals; a whole number for a piece product (see Quantities above). The merged line quantity may not exceed the policy's max_quantity_per_line. |
configuration | object | conditional | Required for made_to_measure products, forbidden otherwise. Shape: {"dimensions": {...}, "choices": [choice_ids]}, dimensions as whole numbers. Validated by the configurator engine. |
Headers
| Name | Type | Required | Description |
|---|---|---|---|
X-Customer-Token | string | no | Selects the pricing context; required if the cart is bound to a non-guest customer. |
curl -X POST https://api.borkol.com/v1/api/commerce/carts/9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4/lines \
-H "Authorization: Bearer {api_key}" \
-H "Content-Type: application/json" \
-d '{
"product_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"variant_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
"quantity": 2
}'const res = await fetch(
`https://api.borkol.com/v1/api/commerce/carts/${cartId}/lines`,
{
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
product_id: "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
variant_id: "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
quantity: 2,
}),
}
);
const { data: cart } = await res.json();{
"data": {
"id": "9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4",
"status": "active",
"lines": [
{
"id": "5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9",
"product_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"variant_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
"name": "Garden Chair",
"sku": "CHAIR-GRN-M",
"quantity": 2,
"unit_price_amount": 2250,
"unit_price": { "amount": 2250, "currency": "EUR" },
"line_total": { "amount": 4500, "currency": "EUR" },
"attributes": null
}
],
"...": "same cart shape as GET /v1/api/commerce/carts/{id}"
}
}For a made_to_measure product, the created line carries the configuration snapshot. components[].qty is an exact decimal string per configured unit (multiply by the line quantity for the stock reserved):
{
"attributes": {
"configuration": {
"dimensions": { "width": 120, "depth": 180 },
"choices": ["c1d2e3f4-..."],
"breakdown": [
{ "label": "Base", "amount": 18900 },
{ "label": "Motor: Motorised", "amount": 7500 }
],
"components": [{ "product_id": "...", "qty": "2.16" }],
"matrix": { "id": "...", "name": "Fabric blinds" }
}
}
}Errors
Line refusals are 422 errors with the message attached to the offending field in errors and a top-level code (see Refusal codes above):
| Status | Body | When |
|---|---|---|
| 422 | errors.product_id: ["Product is not available."] | Product unknown or not active. No code. |
| 422 | errors.product_id: ["Product is not available."], code: "NOT_PURCHASABLE" | The product exists and is active but cannot be sold, for example it has no resolvable price. |
| 422 | errors.product_id: ["Product is not available."], code: "CAPABILITY_INACTIVE" | The product's capability is not active for the store (capability_active is false). |
| 422 | errors.variant_id: ["Choose a variant for this product."], code: "INVALID_CONFIGURATION" | Product has active variants and variant_id was omitted. |
| 422 | errors.variant_id: ["Variant is not available."], code: "INVALID_CONFIGURATION" | Variant id does not resolve to an active variant of the product. |
| 422 | errors.variant_id: ["This product has no variants."], code: "INVALID_CONFIGURATION" | variant_id sent for a product without variants. |
| 422 | errors.variant_id: ["Configurable products have no variants."], code: "INVALID_CONFIGURATION" | variant_id sent for a made_to_measure product. |
| 422 | errors.configuration: ["Configure this product before adding it."], code: "INVALID_CONFIGURATION" | made_to_measure product without a configuration. |
| 422 | errors.configuration: ["This product is not configurable."], code: "INVALID_CONFIGURATION" | configuration sent for a standard product. |
| 422 | errors.choices, errors.choices.{choiceId}, errors.dimensions.{key}, errors.configuration, code: "INVALID_CONFIGURATION" | Configurator engine errors, attached to their own field keys exactly as documented in the quote error catalog, including {Label} must be a whole number. for a fractional dimension. |
| 422 | errors.quantity: ["The quantity must be a whole number."] | Fractional quantity for a piece product. No code. |
| 422 | errors.quantity: ["..."] | Quantity missing, not a plain decimal (comma, exponent, more than 3 decimals or 8 integer digits), or not greater than zero. No code. |
| 422 | {"message": "Quantity exceeds the per-line maximum.", "code": "cart_constraint"} | Merged quantity would exceed max_quantity_per_line. |
| 422 | {"message": "Cart line limit reached.", "code": "cart_constraint"} | Adding a new distinct line would exceed max_lines. |
| 404 | {"message": "..."} | Cart unknown, expired, or not active. |
| 403 | ownership, scope, module, or suspension body | See the Carts page. |
| 429 | {"message": "Too Many Attempts."} | Rate limit exceeded. |
PATCH /v1/api/commerce/carts/{id}/lines/{lineId}
Changes a line's quantity, under the same quantity rules as adding (a whole number for a piece product). Non-configured product lines are re-priced at the new quantity (tier pricing) using the request's pricing context. Configured lines, and lines whose variant has since been deactivated, keep their stored unit price; checkout is where unavailability surfaces. Returns the full cart with status 200.
Parameters
Path
| Name | Type | Required | Description |
|---|---|---|---|
id | string (UUID) | yes | Cart id. Must be an active cart. |
lineId | string (UUID) | yes | Line id. Must belong to this cart. |
Body
| Name | Type | Required | Description |
|---|---|---|---|
quantity | number | yes | Greater than zero, at most 3 decimals, a whole number for a piece product; maximum the policy's max_quantity_per_line. |
Headers
| Name | Type | Required | Description |
|---|---|---|---|
X-Customer-Token | string | no | Selects the pricing context; required if the cart is bound to a non-guest customer. |
Errors
| Status | Body | When |
|---|---|---|
| 422 | {"message": "...", "errors": {"quantity": ["..."]}} | Missing or malformed quantity, not greater than zero, or fractional for a piece product. This endpoint never answers with a line refusal code: a line that no longer resolves keeps its price until checkout. |
| 422 | {"message": "Quantity exceeds the per-line maximum.", "code": "cart_constraint"} | Above max_quantity_per_line. |
| 404 | {"message": "..."} | Cart unknown or not active, or the line does not belong to this cart. |
| 403 | ownership, scope, module, or suspension body | See the Carts page. |
| 429 | {"message": "Too Many Attempts."} | Rate limit exceeded. |
curl -X PATCH https://api.borkol.com/v1/api/commerce/carts/9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4/lines/5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9 \
-H "Authorization: Bearer {api_key}" \
-H "Content-Type: application/json" \
-d '{"quantity": 3}'{
"data": {
"id": "9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4",
"lines": [{ "id": "5e6f7a8b-...", "quantity": 3, "...": "..." }],
"...": "same cart shape as GET /v1/api/commerce/carts/{id}"
}
}PATCH /v1/api/commerce/carts/{id}/lines/{lineId}
Changes a line's quantity, under the same quantity rules as adding (a whole number for a piece product). Non-configured product lines are re-priced at the new quantity (tier pricing) using the request's pricing context. Configured lines, and lines whose variant has since been deactivated, keep their stored unit price; checkout is where unavailability surfaces. Returns the full cart with status 200.
Parameters
Path
| Name | Type | Required | Description |
|---|---|---|---|
id | string (UUID) | yes | Cart id. Must be an active cart. |
lineId | string (UUID) | yes | Line id. Must belong to this cart. |
Body
| Name | Type | Required | Description |
|---|---|---|---|
quantity | number | yes | Greater than zero, at most 3 decimals, a whole number for a piece product; maximum the policy's max_quantity_per_line. |
Headers
| Name | Type | Required | Description |
|---|---|---|---|
X-Customer-Token | string | no | Selects the pricing context; required if the cart is bound to a non-guest customer. |
curl -X PATCH https://api.borkol.com/v1/api/commerce/carts/9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4/lines/5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9 \
-H "Authorization: Bearer {api_key}" \
-H "Content-Type: application/json" \
-d '{"quantity": 3}'{
"data": {
"id": "9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4",
"lines": [{ "id": "5e6f7a8b-...", "quantity": 3, "...": "..." }],
"...": "same cart shape as GET /v1/api/commerce/carts/{id}"
}
}Errors
| Status | Body | When |
|---|---|---|
| 422 | {"message": "...", "errors": {"quantity": ["..."]}} | Missing or malformed quantity, not greater than zero, or fractional for a piece product. This endpoint never answers with a line refusal code: a line that no longer resolves keeps its price until checkout. |
| 422 | {"message": "Quantity exceeds the per-line maximum.", "code": "cart_constraint"} | Above max_quantity_per_line. |
| 404 | {"message": "..."} | Cart unknown or not active, or the line does not belong to this cart. |
| 403 | ownership, scope, module, or suspension body | See the Carts page. |
| 429 | {"message": "Too Many Attempts."} | Rate limit exceeded. |
DELETE /v1/api/commerce/carts/{id}/lines/{lineId}
Removes a line from the cart and releases any stock hold held for the cart. Returns the full cart, without the removed line, with status 200.
Parameters
Path
| Name | Type | Required | Description |
|---|---|---|---|
id | string (UUID) | yes | Cart id. Must be an active cart. |
lineId | string (UUID) | yes | Line id. Must belong to this cart. |
No body.
Errors
| Status | Body | When |
|---|---|---|
| 404 | {"message": "..."} | Cart unknown or not active, or the line does not belong to this cart. |
| 403 | ownership, scope, module, or suspension body | See the Carts page. |
| 429 | {"message": "Too Many Attempts."} | Rate limit exceeded. |
curl -X DELETE https://api.borkol.com/v1/api/commerce/carts/9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4/lines/5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9 \
-H "Authorization: Bearer {api_key}"{
"data": {
"id": "9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4",
"lines": [],
"...": "same cart shape as GET /v1/api/commerce/carts/{id}"
}
}DELETE /v1/api/commerce/carts/{id}/lines/{lineId}
Removes a line from the cart and releases any stock hold held for the cart. Returns the full cart, without the removed line, with status 200.
Parameters
Path
| Name | Type | Required | Description |
|---|---|---|---|
id | string (UUID) | yes | Cart id. Must be an active cart. |
lineId | string (UUID) | yes | Line id. Must belong to this cart. |
No body.
curl -X DELETE https://api.borkol.com/v1/api/commerce/carts/9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4/lines/5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9 \
-H "Authorization: Bearer {api_key}"{
"data": {
"id": "9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4",
"lines": [],
"...": "same cart shape as GET /v1/api/commerce/carts/{id}"
}
}Errors
| Status | Body | When |
|---|---|---|
| 404 | {"message": "..."} | Cart unknown or not active, or the line does not belong to this cart. |
| 403 | ownership, scope, module, or suspension body | See the Carts page. |
| 429 | {"message": "Too Many Attempts."} | Rate limit exceeded. |