borkoldocs

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 Markdown

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):

  • standard products with active variants: variant_id is required and must be an active variant of the product.
  • standard products without variants: variant_id must be omitted.
  • made_to_measure products: configuration is required and variant_id is forbidden. The configuration is an object {"dimensions": {...}, "choices": ["choice_id", ...]} priced by the configurator engine; dimensions are whole numbers. The line stores a config_hash plus a full snapshot under attributes.configuration (dimensions, choices, price breakdown, component list, matrix cell). Component stock is reserved per configurator component.
  • For standard products, configuration is forbidden.
  • A product whose capability_active is false cannot be added: the commerce module sells standard products and the configurator module sells made_to_measure ones.

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:

codeMeaning
CAPABILITY_INACTIVEThe 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_CONFIGURATIONThe 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_PURCHASABLEThe product cannot be sold, for example it has no resolvable price. Reported on errors.product_id.
Response (422, line refusal)
{
  "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

NameTypeRequiredDescription
idstring (UUID)yesCart id. Must be an active cart.

Body

NameTypeRequiredDescription
product_idstring (UUID)yesThe product to add. Must be an active product of the store.
variant_idstring (UUID) or nullconditionalRequired when the product has active variants; forbidden otherwise, and always forbidden for made_to_measure products.
quantitynumberyesGreater 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.
configurationobjectconditionalRequired for made_to_measure products, forbidden otherwise. Shape: {"dimensions": {...}, "choices": [choice_ids]}, dimensions as whole numbers. Validated by the configurator engine.

Headers

NameTypeRequiredDescription
X-Customer-TokenstringnoSelects the pricing context; required if the cart is bound to a non-guest customer.
Request
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
  }'
Request (JS)
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();
Response
{
  "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):

Response (made-to-measure line excerpt)
{
  "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):

StatusBodyWhen
422errors.product_id: ["Product is not available."]Product unknown or not active. No code.
422errors.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.
422errors.product_id: ["Product is not available."], code: "CAPABILITY_INACTIVE"The product's capability is not active for the store (capability_active is false).
422errors.variant_id: ["Choose a variant for this product."], code: "INVALID_CONFIGURATION"Product has active variants and variant_id was omitted.
422errors.variant_id: ["Variant is not available."], code: "INVALID_CONFIGURATION"Variant id does not resolve to an active variant of the product.
422errors.variant_id: ["This product has no variants."], code: "INVALID_CONFIGURATION"variant_id sent for a product without variants.
422errors.variant_id: ["Configurable products have no variants."], code: "INVALID_CONFIGURATION"variant_id sent for a made_to_measure product.
422errors.configuration: ["Configure this product before adding it."], code: "INVALID_CONFIGURATION"made_to_measure product without a configuration.
422errors.configuration: ["This product is not configurable."], code: "INVALID_CONFIGURATION"configuration sent for a standard product.
422errors.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.
422errors.quantity: ["The quantity must be a whole number."]Fractional quantity for a piece product. No code.
422errors.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.
403ownership, scope, module, or suspension bodySee 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

NameTypeRequiredDescription
idstring (UUID)yesCart id. Must be an active cart.
lineIdstring (UUID)yesLine id. Must belong to this cart.

Body

NameTypeRequiredDescription
quantitynumberyesGreater than zero, at most 3 decimals, a whole number for a piece product; maximum the policy's max_quantity_per_line.

Headers

NameTypeRequiredDescription
X-Customer-TokenstringnoSelects the pricing context; required if the cart is bound to a non-guest customer.
Request
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}'
Response
{
  "data": {
    "id": "9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4",
    "lines": [{ "id": "5e6f7a8b-...", "quantity": 3, "...": "..." }],
    "...": "same cart shape as GET /v1/api/commerce/carts/{id}"
  }
}

Errors

StatusBodyWhen
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.
403ownership, scope, module, or suspension bodySee 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

NameTypeRequiredDescription
idstring (UUID)yesCart id. Must be an active cart.
lineIdstring (UUID)yesLine id. Must belong to this cart.

No body.

Request
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}"
Response
{
  "data": {
    "id": "9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4",
    "lines": [],
    "...": "same cart shape as GET /v1/api/commerce/carts/{id}"
  }
}

Errors

StatusBodyWhen
404{"message": "..."}Cart unknown or not active, or the line does not belong to this cart.
403ownership, scope, module, or suspension bodySee the Carts page.
429{"message": "Too Many Attempts."}Rate limit exceeded.