borkoldocs

Carts & Checkout

Carts

Create, read, and update carts, including ownership token semantics and the full cart response shape.

View as Markdown

A cart is the mutable pre-order object a storefront builds up before checkout. Every cart belongs to one store, has a UUID id, and is always created in currency EUR with a 30-day expiry. Every write to a cart (adding lines, changing email, attaching a discount code) extends the expiry by another 30 days and updates the cart's last-activity timestamp. Expired carts are physically deleted; an expired cart id returns 404.

All cart endpoints require the Authorization: Bearer {api_key} header. Read endpoints need the commerce:read scope, write endpoints need commerce:write, and the commerce module must be enabled for the store. The tenant rate limit of 120 requests per minute applies.

Cart lifecycle and status

StatusMeaning
activeThe cart accepts writes. This is the only status a cart is ever created in.
convertedThe cart became an order (checkout) or a quote (quote request). Read-only.
mergedThe cart was folded into another cart during a claim. Read-only.

Write endpoints look the cart up with status = active, so a converted or merged cart id returns 404 on every write. The exceptions are GET /v1/api/commerce/carts/{id}, which returns a cart in any status, and POST /v1/api/commerce/carts/{id}/checkout, which returns the existing order for an already-converted cart (idempotent replay).

Ownership: the X-Customer-Token header

Carts start anonymous. The optional X-Customer-Token header carries an opaque customer session token (issued by the customers module) and affects cart routes in three ways:

  1. Pricing. When present and valid, prices are resolved in that customer's pricing context (customer-group pricing, B2B price display). Without it, guest pricing applies.
  2. Ownership. If a cart's customer_id is set and that customer is a real (non-guest) account, every cart-scoped request must carry an X-Customer-Token that resolves to that same customer. Otherwise the request fails with 403 {"message": "This cart belongs to a customer."}. Carts with customer_id null stay open, and so do carts bound to a guest customer row created by email capture (guests have no token to present). If the customers module is unavailable, the check fails closed with the same 403.
  3. Binding. POST /v1/api/commerce/carts/{id}/claim requires the header, and at checkout a valid token binds the resulting order to the customer.

A token that is present but does not resolve returns 401 {"message": "Invalid customer token."} on checkout, quote request, and order history, and a plain 401 on claim. On the other cart routes an unresolvable token simply falls back to guest pricing unless the cart is customer-bound (then the ownership 403 applies).

The cart response shape

Every cart endpoint (and several cart-line and discount endpoints) returns the same envelope: {"data": {...cart...}}. Fields:

NameTypeRequiredDescription
idstring (UUID)alwaysCart id. Store it client-side; it is the visitor's handle.
currencystringalwaysAlways EUR.
statusstringalwaysactive, converted, or merged.
emailstring or nullalwaysEmail captured via PATCH /carts/{id}, else null.
metadataobject or nullalwaysFree-form scalar map set via PATCH /carts/{id}, else null.
discount_codestring or nullalwaysThe attached code, stored verbatim, else null.
discount_code_errorstring or nullalwaysNull when the code applies (or no code is set). Otherwise one of invalid, expired, min_order, not_eligible, usage_limit.
discountsarrayalwaysApplied discounts, each {id, name, code, kind, amount}. kind is one of order, products, shipping, bogo; code is null for automatic discounts; amount is an integer in minor units.
policyobjectalwaysThe store's cart policy, see below.
requires_quotebooleanalwaysTrue when the cart currently trips a quote trigger and checkout will refuse with QUOTE_REQUIRED.
quote_reasonsarray of stringsalwaysActive trigger reasons: cart_threshold, configurator_choice, customer_group, product_purchasability. Empty when requires_quote is false.
linesarrayalwaysCart lines, see below.
totalsobjectalwaysCart totals, see below.

policy

The store's cart policy, evaluated live from tenant settings. Storefronts should render against it (for example, hide prices for guests, require login before checkout).

NameTypeRequiredDescription
identity_modestringalwaysguest_allowed, login_required, or guest_only.
audiencestringalwaysboth, b2b_only, or b2c_only.
min_order_value_amountinteger or nullalwaysMinimum cart total in minor units required to check out, or null for no minimum.
max_linesintegeralwaysMaximum number of distinct lines. Default 100.
max_quantity_per_lineintegeralwaysMaximum quantity per line. Default 100.
pricingobjectalwaysPrice display block for the current pricing context.
pricing.guest_price_visibilitystringalwaysvisible or hidden.
pricing.vat_displaystringalwaysinc or ex. Resolves to ex automatically for B2B customers.
pricing.show_original_pricebooleanalwaysWhether to show the pre-discount price next to a discounted one.
pricing.show_tier_tablebooleanalwaysWhether to show quantity tier price tables.

lines[]

NameTypeRequiredDescription
idstring (UUID)alwaysLine id.
product_idstring (UUID)alwaysThe product.
variant_idstring (UUID) or nullalwaysThe variant, when the product has variants.
namestringalwaysProduct name snapshot taken when the line was added.
skustring or nullalwaysSKU snapshot.
quantityintegeralwaysQuantity, at least 1.
unit_price_amountintegeralwaysUnit price in minor units (cents).
unit_priceobjectalwaysThe same unit price as a money object {"amount": int, "currency": "EUR"}.
line_totalobjectalwaysLine total money object, after discount allocation.
attributesobject or nullalwaysNull for plain lines. For configurable products this holds the full configuration snapshot (dimensions, choices, breakdown, components, matrix).

totals

All money values are objects {"amount": int, "currency": "EUR"} with amount in minor units. Whether prices include tax is a store setting (prices_include_tax, default true).

NameTypeRequiredDescription
subtotalmoneyalwaysGoods total excluding tax.
tax_totalmoneyalwaysTotal tax.
totalmoneyalwaysGrand total.
discount_totalmoneyalwaysTotal goods discount applied.
tax_breakdownarrayalwaysPer-rate breakdown, each {"rate": float, "base": int, "tax": int} with base and tax in minor units.

POST /v1/api/commerce/carts

Creates a new empty cart. Requires commerce:write. There is no request body and no customer token is needed: carts start anonymous. The cart is created with currency EUR, status active, and a 30-day expires_at. Persist the returned id client-side (cookie or local storage); it is the only handle to the cart.

Parameters

None.

Request
curl -X POST https://api.borkol.com/v1/api/commerce/carts \
  -H "Authorization: Bearer {api_key}"
Request (JS)
const res = await fetch("https://api.borkol.com/v1/api/commerce/carts", {
  method: "POST",
  headers: { Authorization: `Bearer ${apiKey}` },
});
const { data: cart } = await res.json();
// persist cart.id
Response
{
  "data": {
    "id": "9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4",
    "currency": "EUR",
    "status": "active",
    "email": null,
    "metadata": null,
    "discount_code": null,
    "discount_code_error": null,
    "discounts": [],
    "policy": {
      "identity_mode": "guest_allowed",
      "audience": "both",
      "min_order_value_amount": null,
      "max_lines": 100,
      "max_quantity_per_line": 100,
      "pricing": {
        "guest_price_visibility": "visible",
        "vat_display": "inc",
        "show_original_price": false,
        "show_tier_table": true
      }
    },
    "requires_quote": false,
    "quote_reasons": [],
    "lines": [],
    "totals": {
      "subtotal": { "amount": 0, "currency": "EUR" },
      "tax_total": { "amount": 0, "currency": "EUR" },
      "total": { "amount": 0, "currency": "EUR" },
      "discount_total": { "amount": 0, "currency": "EUR" },
      "tax_breakdown": []
    }
  }
}

The response status is 201.

Errors

StatusBodyWhen
401{"message": "Unauthenticated."}Missing or invalid API key.
403{"message": "Invalid ability provided."}Key lacks commerce:write.
403{"error": "Module not enabled: commerce"}Commerce module disabled for the store.
403{"message": "Tenant suspended.", "code": "tenant_suspended"}Store suspended.
429{"message": "Too Many Attempts."}Rate limit exceeded.

GET /v1/api/commerce/carts/{id}

Fetches a cart with lines, live-evaluated discounts, totals, the store's cart policy block, and the current quote-trigger state. Requires commerce:read. Unlike write endpoints, this works for a cart in any status (active, converted, merged). Discounts and discount_code_error are re-evaluated on every read, so this endpoint is the source of truth for rendering the cart. Cart ownership is enforced (see above).

Parameters

Path

NameTypeRequiredDescription
idstring (UUID)yesCart id.

Headers

NameTypeRequiredDescription
X-Customer-TokenstringnoRequired if the cart is bound to a non-guest customer. Also selects customer-group pricing for displayed prices.
Request
curl https://api.borkol.com/v1/api/commerce/carts/9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4 \
  -H "Authorization: Bearer {api_key}"
Response
{
  "data": {
    "id": "9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4",
    "currency": "EUR",
    "status": "active",
    "email": "jane@example.com",
    "metadata": { "source": "web" },
    "discount_code": "WELCOME10",
    "discount_code_error": null,
    "discounts": [
      {
        "id": "71c9e0aa-2f4b-4d31-9a77-0b1c2d3e4f5a",
        "name": "Welcome 10%",
        "code": "WELCOME10",
        "kind": "order",
        "amount": 450
      }
    ],
    "policy": {
      "identity_mode": "guest_allowed",
      "audience": "both",
      "min_order_value_amount": null,
      "max_lines": 100,
      "max_quantity_per_line": 100,
      "pricing": {
        "guest_price_visibility": "visible",
        "vat_display": "inc",
        "show_original_price": false,
        "show_tier_table": true
      }
    },
    "requires_quote": false,
    "quote_reasons": [],
    "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": 4050, "currency": "EUR" },
        "attributes": null
      }
    ],
    "totals": {
      "subtotal": { "amount": 3347, "currency": "EUR" },
      "tax_total": { "amount": 703, "currency": "EUR" },
      "total": { "amount": 4050, "currency": "EUR" },
      "discount_total": { "amount": 450, "currency": "EUR" },
      "tax_breakdown": [{ "rate": 21, "base": 3347, "tax": 703 }]
    }
  }
}

Errors

StatusBodyWhen
404{"message": "..."}Unknown id, expired (pruned) cart, or a cart belonging to another store.
403{"message": "This cart belongs to a customer."}Cart is customer-bound and the token is missing or resolves to a different customer.
403scope, module, or suspension bodySee POST /carts.
429{"message": "Too Many Attempts."}Rate limit exceeded.

PATCH /v1/api/commerce/carts/{id}

Updates the cart's email and/or metadata. Requires commerce:write; the cart must be active; ownership is enforced. Setting email lowercases it and, when the customers module is active, finds or creates a guest customer row and binds the cart to it (or binds to the token's account when a valid X-Customer-Token proves ownership). This binding is the basis for abandoned-cart recovery; a guest-bound cart remains accessible without a token.

Parameters

Path

NameTypeRequiredDescription
idstring (UUID)yesCart id.

Body

NameTypeRequiredDescription
emailstringnoValid email address, max 255 characters. Stored lowercased.
metadataobject or nullnoFree-form map, max 16 keys. Every value must be a scalar (string, number, boolean) or null. Pass null to clear.

Both fields are sometimes rules: omitting a field leaves it unchanged.

Headers

NameTypeRequiredDescription
X-Customer-TokenstringnoRequired 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 \
  -H "Authorization: Bearer {api_key}" \
  -H "Content-Type: application/json" \
  -d '{"email": "jane@example.com", "metadata": {"utm_source": "newsletter"}}'
Response
{
  "data": {
    "id": "9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4",
    "email": "jane@example.com",
    "metadata": { "utm_source": "newsletter" },
    "...": "same cart shape as GET /v1/api/commerce/carts/{id}"
  }
}

The response is the full cart envelope, identical in shape to GET /v1/api/commerce/carts/{id}, with status 200.

Errors

StatusBodyWhen
422{"message": "...", "errors": {"email": ["The email field must be a valid email address."]}}Validation failure (invalid email, too many metadata keys, non-scalar metadata value).
404{"message": "..."}Cart unknown, expired, or not active.
403{"message": "This cart belongs to a customer."}Ownership check failed.
403scope, module, or suspension bodySee POST /carts.
429{"message": "Too Many Attempts."}Rate limit exceeded.