Carts & Checkout
Cart lines
Add products to a cart, change line quantities, and remove lines, including variant and configurable-product 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 configurable 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 configurable 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 type rules
How a product is added depends on its type (simple, variable, configurable, digital, license, subscription):
- Products with active variants (typically
variable):variant_idis required and must be an active variant of the product. - Products without variants (
simple,digital,license,subscription):variant_idmust be omitted. - Configurable products:
configurationis required andvariant_idis forbidden. The configuration is an object{"dimensions": {...}, "choices": ["choice_id", ...]}priced by the configurator engine. 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 all other products,
configurationis forbidden.
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.
Product type rules
How a product is added depends on its type (simple, variable, configurable, digital, license, subscription):
- Products with active variants (typically
variable):variant_idis required and must be an active variant of the product. - Products without variants (
simple,digital,license,subscription):variant_idmust be omitted. - Configurable products:
configurationis required andvariant_idis forbidden. The configuration is an object{"dimensions": {...}, "choices": ["choice_id", ...]}priced by the configurator engine. 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 all other products,
configurationis forbidden.
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 configurable products. |
quantity | integer | yes | Minimum 1. The merged line quantity may not exceed the policy's max_quantity_per_line. |
configuration | object | conditional | Required for configurable products, forbidden otherwise. Shape: {"dimensions": {...}, "choices": [choice_ids]}. 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 configurable product, the created line carries the configuration snapshot:
Errors
Domain failures are standard 422 validation errors with the message attached to the offending field in errors:
| Status | Body | When |
|---|---|---|
| 422 | errors.product_id: ["Product is not available."] | Product missing, inactive, or (for configurable products) the configurator engine is absent. |
| 422 | errors.variant_id: ["Choose a variant for this product."] | Product has active variants and variant_id was omitted. |
| 422 | errors.variant_id: ["Variant is not available."] | Variant id does not resolve to an active variant of the product. |
| 422 | errors.variant_id: ["This product has no variants."] | variant_id sent for a product without variants. |
| 422 | errors.variant_id: ["Configurable products have no variants."] | variant_id sent for a configurable product. |
| 422 | errors.configuration: ["Configure this product before adding it."] | Configurable product without a configuration. |
| 422 | errors.configuration: ["This product is not configurable."] | configuration sent for a non-configurable product. |
| 422 | errors.choices, errors.choices.{choiceId}, errors.dimensions.{key}, errors.configuration | Configurator engine errors, attached to their own field keys exactly as documented in the quote error catalog. |
| 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": 300, "depth": 250 },
"choices": ["c1d2e3f4-..."],
"breakdown": [],
"components": [{ "product_id": "...", "qty": 2 }],
"matrix": {}
}
}
}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 configurable products. |
quantity | integer | yes | Minimum 1. The merged line quantity may not exceed the policy's max_quantity_per_line. |
configuration | object | conditional | Required for configurable products, forbidden otherwise. Shape: {"dimensions": {...}, "choices": [choice_ids]}. 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 configurable product, the created line carries the configuration snapshot:
{
"attributes": {
"configuration": {
"dimensions": { "width": 300, "depth": 250 },
"choices": ["c1d2e3f4-..."],
"breakdown": [],
"components": [{ "product_id": "...", "qty": 2 }],
"matrix": {}
}
}
}Errors
Domain failures are standard 422 validation errors with the message attached to the offending field in errors:
| Status | Body | When |
|---|---|---|
| 422 | errors.product_id: ["Product is not available."] | Product missing, inactive, or (for configurable products) the configurator engine is absent. |
| 422 | errors.variant_id: ["Choose a variant for this product."] | Product has active variants and variant_id was omitted. |
| 422 | errors.variant_id: ["Variant is not available."] | Variant id does not resolve to an active variant of the product. |
| 422 | errors.variant_id: ["This product has no variants."] | variant_id sent for a product without variants. |
| 422 | errors.variant_id: ["Configurable products have no variants."] | variant_id sent for a configurable product. |
| 422 | errors.configuration: ["Configure this product before adding it."] | Configurable product without a configuration. |
| 422 | errors.configuration: ["This product is not configurable."] | configuration sent for a non-configurable product. |
| 422 | errors.choices, errors.choices.{choiceId}, errors.dimensions.{key}, errors.configuration | Configurator engine errors, attached to their own field keys exactly as documented in the quote error catalog. |
| 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. 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 | integer | yes | Minimum 1, 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 non-integer quantity, or below 1. |
| 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. 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 | integer | yes | Minimum 1, 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 non-integer quantity, or below 1. |
| 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. |