# Changelog

The storefront API is versioned by URL prefix (`/v1`). Within v1 every change is additive: fields, endpoints, optional parameters and enum values are only ever added. Each entry below names the date it shipped, what changed, and which pages of this reference were updated. The same entries appear in the repository changelog.

## 2026-10-11

### Added
- `capability` (`standard` or `made_to_measure`) and `capability_active` (boolean) on the product object of `GET /v1/api/commerce/products` and `GET /v1/api/commerce/products/{id}`. `capability` says how the product goes into a cart; `capability_active` is `false` while the module that sells it (commerce for `standard`, configurator for `made_to_measure`) is disabled. See [Products](/api/catalog/products).
- `unit` on the product object (`piece`, `kg`, `g`, `l`, `m`, `m2`, `m3`, `hour`) and on every order line (`lines[].unit`, the product's unit at order time). See [Products](/api/catalog/products) and [Orders](/api/orders-and-quotes/orders).
- Product fields `compare_at_amount`, `barcode`, `weight_grams`, `requires_shipping`, `track_stock`, `returnable`, `lead_time_days`, `tags` and `social_image`, and variant fields `barcode` and `weight_grams`. See [Products](/api/catalog/products).
- `GET /v1/api/redirects?path=` returns `{data: {from_path, to_path, status}}` for a product or category path whose slug changed (404 when none), and `GET /v1/api/sitemap` lists every active product and category path as `{data: [{type, path, lastmod}]}`. Both take `catalog:read` or `commerce:read` and the catalog module. See [Redirects and sitemap](/api/catalog/seo).
- A top-level `code` on the 422 a cart line add refuses with (`POST /v1/api/commerce/carts/{id}/lines`): `CAPABILITY_INACTIVE`, `INVALID_CONFIGURATION` or `NOT_PURCHASABLE`, next to the unchanged `message` and `errors`. See [Cart lines](/api/carts-and-checkout/cart-lines) and [Errors and rate limits](/api/getting-started/errors-and-rate-limits).
- `attributes.returnable` and `attributes.lead_time_days` on every order line placed through checkout or an accepted quote; a made-to-measure line is never returnable. See [Orders](/api/orders-and-quotes/orders).

### Changed
- **Breaking:** `type` is removed from the product object. Use `capability` instead, plus `requires_shipping` and `track_stock`: former `simple` and `variable` products are `standard`; `configurable` products are `made_to_measure` (and not returnable); `digital`, `license` and `subscription` products are `standard` with `requires_shipping: false` and `track_stock: false`. See [Products](/api/catalog/products).
- **Breaking:** quantities are decimal. Cart line, order line and quote line `quantity`, availability `available`, the checkout and quote-accept `out_of_stock[].requested` and `.available`, and the `stock.adjusted` webhook `quantity` are JSON numbers that may be fractional (`0.75`, `12`). `quantity` on `POST /v1/api/commerce/carts/{id}/lines` and `PATCH .../lines/{lineId}` accepts up to three decimals; a product with `unit` `piece` still takes whole numbers only (422 on `quantity`). See [Cart lines](/api/carts-and-checkout/cart-lines), [Carts](/api/carts-and-checkout/carts), [Orders](/api/orders-and-quotes/orders), [Availability](/api/stock/availability) and [Webhooks](/api/webhooks/index).
- **Breaking:** the configurator endpoints (`GET /v1/api/commerce/configurator/products/{id}/schema`, `POST .../quote`) are gated by the configurator module: without it they answer `403 {"error":"Module not enabled: configurator"}` (was `catalog`). They answer 404 for a product whose `capability` is not `made_to_measure`. See [Configuration schema](/api/configurator/schema), [Price quote](/api/configurator/quote) and [How the configurator works](/api/configurator/index).
- Configurator dimensions must be whole numbers; a fractional value used to be truncated and is now refused. The quote endpoint answers 422 on `dimensions.{key}` (`{Label} must be a whole number.`) in its plain validation envelope, without a top-level `code`; a cart add answers the same error with `code` `INVALID_CONFIGURATION`. See [Price quote](/api/configurator/quote) and [Cart lines](/api/carts-and-checkout/cart-lines).
- **Breaking:** `components[].qty` in the configuration snapshot on cart and order lines is an exact decimal string (`"2.16"`), was an integer. See [Carts](/api/carts-and-checkout/carts) and [Orders](/api/orders-and-quotes/orders).
- **Breaking:** `POST /v1/api/commerce/carts/{id}/quote-request` belongs to the sales module and answers `404 {"error":"Module not enabled: sales"}` when the store has sales disabled. Quote triggers fire only when sales is enabled and quote requests are switched on; otherwise `requires_quote` is always false and checkout never refuses with `QUOTE_REQUIRED`. See [Quote requests](/api/orders-and-quotes/quote-requests), [Checkout](/api/carts-and-checkout/checkout) and [Errors and rate limits](/api/getting-started/errors-and-rate-limits).
- `purchasability` is set by the sales module and reads `direct` when the merchant set nothing; it is reported even when sales is disabled. A `quote` product can be added to a cart: the cart reports `requires_quote: true` with reason `product_purchasability`, checkout refuses with `409 {"code": "QUOTE_REQUIRED", "reasons": [...]}`, and the cart goes through the quote request instead. A contact group's always-quote setting is now a sales setting; the `customer_group` reason is unchanged. See [Products](/api/catalog/products) and [Quote requests](/api/orders-and-quotes/quote-requests).
- `stock_status` is `untracked` for a product with `track_stock: false` and all of its variants, even when stock rows exist, on the product object and on `GET /v1/api/stock/availability` and `GET /v1/api/stock/products/{productId}` (`tracked: false`). See [Products](/api/catalog/products) and [Availability](/api/stock/availability).
- Checkout requires `shipping_method_id` when the cart holds a line whose product has `requires_shipping: true` (was: a product of type `simple`, `variable` or `configurable`). See [Checkout](/api/carts-and-checkout/checkout) and [Shipping methods](/api/carts-and-checkout/shipping-methods).
- Quote lines created from a cart carry `unit` `pcs` for a product sold by the piece and the product's unit otherwise (was `null`). See [Quote requests](/api/orders-and-quotes/quote-requests) and [Quote tokens](/api/orders-and-quotes/quote-tokens).
- Accepting a quote in order mode reserves stock through each product's line handler at the quoted price, so a made-to-measure line deducts its components. Only a real shortage refuses with `OUT_OF_STOCK`; a line whose product can no longer be sold converts without stock movement. See [Quote tokens](/api/orders-and-quotes/quote-tokens).

### Deprecated
- `QUOTE_NOT_ORDERABLE` (422) on `POST /v1/api/sales/quotes/by-token/{token}/accept` and `POST /v1/api/commerce/quotes/by-token/{token}/accept` is no longer returned: a quote with fractional line quantities converts to an order with those quantities. Keep tolerating the code, but expect never to see it. See [Quote tokens](/api/orders-and-quotes/quote-tokens).

### Notes
- The `/v1` surface was restructured before any production consumer depended on it, so the breaking changes above ship inside `/v1` instead of under a new URL version. From this release on, changes within `/v1` are additive again.
- Documentation fixes, nothing changed in the API: the product and category endpoints accept `catalog:read` or `commerce:read` and are gated by the catalog module (the reference named `commerce:read` and `commerce`); the order object's `invoice_id` and the full quote request response (`origin`, `intro`, `terms`, `acceptance_mode`, `pdf_url`, `signed_at`, `signed_pdf_url`, and the line fields `kind`, `unit`, `description`, `tax_rate`, `unit_price`) are now documented. See [Products](/api/catalog/products), [Categories](/api/catalog/categories), [Orders](/api/orders-and-quotes/orders) and [Quote requests](/api/orders-and-quotes/quote-requests).

## 2026-09-17

### Added
- `discount_total` (money) and `discount_breakdown` (array or null) on the order object returned by `POST /v1/api/commerce/carts/{id}/checkout`, `GET /v1/api/commerce/orders/{id}` and `GET /v1/api/commerce/my/orders`. `discount_total.amount` is the goods discount applied at checkout (`0` when none); `discount_breakdown` lists one `{discount_id, name, code, kind, amount}` entry per applied discount and is `null` when none applied. Both are additive; no existing field changed. See [Orders](/api/orders-and-quotes/orders) and [Checkout](/api/carts-and-checkout/checkout).
- `message` on the `OUT_OF_STOCK` refusal of `POST /v1/api/commerce/quotes/by-token/{token}/accept` (order mode only). The 409 body now reads `{"code": "OUT_OF_STOCK", "message": "Insufficient stock.", "out_of_stock": [...]}`; the `out_of_stock` rows are unchanged. Additive, and the envelope now matches every other refusal on the endpoint (`code` + `message`). See [Quote tokens](/api/orders-and-quotes/quote-tokens).

### Changed
- `POST /v1/api/commerce/carts/{id}/claim` now merges **every** other `active` cart the customer owns into the claimed cart, not only the first one found. Each folded cart flips to `merged` and releases its stock holds, exactly as before; there is simply no longer an orphaned active cart left behind. See [Claiming a cart](/api/carts-and-checkout/claim) and [Storefront guide](/api/storefront-guide/index).
- The claim merge carries identity across: the claimed cart keeps its own `email` and `metadata` when set and otherwise takes the merged cart's value, so a guest email captured before login is no longer lost. No field was added or retyped on the cart envelope. See [Claiming a cart](/api/carts-and-checkout/claim).

### Notes
- `POST /v1/api/commerce/carts/{id}/lines`: two simultaneous identical adds (same product, variant and configuration) used to make the loser fail with a 500. The loser now folds its quantity into the winning line and returns the same 201 envelope as before, still clamped to `max_quantity_per_line`. See [Cart lines](/api/carts-and-checkout/cart-lines).
- `X-Customer-Token` handling is now consistent with the documented rule "must resolve to a customer, else 401": a token past its `expires_at`, or older than the platform session window, no longer resolves, so it cannot claim or check out a customer-owned cart. Log in again to obtain a fresh token. See [Claiming a cart](/api/carts-and-checkout/claim) and [Checkout](/api/carts-and-checkout/checkout).
- `POST /v1/api/customers/forgot-password` still answers 202 with the same body for known and unknown addresses; it now also does the same amount of queue work on both paths, so the response time no longer hints at whether an account exists. See [Passwords](/api/customers/passwords).
- A `bogo` discount now grants its get quantity once per qualification instead of once per cart: the discounted units are `get_quantity × floor(qualifying buy quantity ÷ minimum buy quantity)`, capped at the quantity of the cheapest get line. The `discounts[].amount` on the cart and the order's `discount_total` grow accordingly; no field or enum value changed. See [Cart discounts](/api/carts-and-checkout/discounts).
- Products of type `configurable` are priced by the configurator engine only; customer-group and tier price rows left over from a type change no longer surface in `price`, `quantity_tiers` or the cart line price for such a product. Simple products are unaffected. See [Products](/api/catalog/products).
- `POST /v1/api/commerce/quotes/by-token/{token}/accept`: the `QUOTE_NOT_OFFERED` refusal raised while the order is being built (a race with another accept of the same quote) now carries the documented message `Quote is not awaiting a response (status: ...)` instead of `Quote cannot be accepted in its current status (...)`. Code and status are unchanged. See [Quote tokens](/api/orders-and-quotes/quote-tokens).

## 2026-09-04

### Changed
- A configurator choice whose `requires` rule cannot be satisfied by the other published choices is now omitted from `GET /v1/api/commerce/configurator/products/{id}/schema` instead of being published and then rejected at quote time. The filtering propagates, so every choice the schema exposes can be selected and priced. See [Configuration schema](/api/configurator/schema) and [How the configurator works](/api/configurator/index).
- `POST /v1/api/commerce/configurator/products/{id}/quote` applies the same availability rule: selecting a choice the schema no longer publishes fails with the existing generic `choices` message, and the per-choice `choices.{choiceId}` messages now name only choices that are still available, so a deactivated choice is never disclosed in an error. See [Price quote](/api/configurator/quote).
- A price matrix with a condition on a choice that is not available is now omitted from `price_matrices` instead of being listed with a shortened `conditions` array. Such a matrix can never become eligible, so the engine treats the configuration as `not_priceable` rather than falling back to it. See [Configuration schema](/api/configurator/schema).

### Notes
- Documentation fix on both configurator endpoints: they accept an API key carrying **either** the `catalog:read` or the `commerce:read` scope (unchanged since 2026-08-25), and their module gate is the catalog module, so the 403 body reads `{"error":"Module not enabled: catalog"}`. The reference named `commerce:read` and `commerce` for both. Nothing changed in the API. See [Price quote](/api/configurator/quote), [Configuration schema](/api/configurator/schema) and [How the configurator works](/api/configurator/index).

## 2026-08-26

### Added
- Customer endpoints are now also served at `/v1/api/contacts/*`; the previous `/v1/api/customers/*` paths remain as a permanent alias with identical request and response shapes. New API-key scopes `contacts:read` and `contacts:write` are available; existing `customers:read` / `customers:write` keys keep working unchanged. See [Contact accounts](/api/customers/index).
- The contact profile (`GET/PATCH /v1/api/contacts/me`, also `.../customers/me`) now includes `kind` (`person` or `company`), `company_name`, `registration_number`, `vat_id`, `external_id`, and `display_name`. These fields are read-only through the storefront API. `email` can now be `null` for any merchant-created contact; storefront registration still requires it. See [Profile](/api/customers/profile).
- The address `type` field accepts a new value, `work`, alongside `shipping` and `billing`. See [Addresses](/api/customers/addresses).
- `contact_id` on order and merchant quote objects: the same value as `customer_id`, under the canonical name going forward. `customer_id` is unchanged and stays. The token-addressed quote object carries neither. See [Orders](/api/orders-and-quotes/orders) and [Checkout](/api/carts-and-checkout/checkout).
- The quote-token endpoints are now also served at `/v1/api/sales/quotes/by-token/{token}` (`GET`, `/accept`, `/decline`). `/v1/api/commerce/quotes/by-token/*` keeps working, identically and permanently; both paths accept a key with `sales:read`/`sales:write` **or** `commerce:read`/`commerce:write`. See [Quote tokens](/api/orders-and-quotes/quote-tokens).
- `GET /v1/api/{sales,commerce}/quotes/by-token/{token}` returns `origin`, `intro`, `terms`, `scope_items[]`, `acceptance_mode`, `pdf_url`, `signed_at` and `signed_pdf_url`; each entry of `lines[]` gains `kind`, `unit`, `description` and `tax_rate`. See [Quote tokens](/api/orders-and-quotes/quote-tokens).
- `POST /v1/api/{sales,commerce}/quotes/by-token/{token}/accept` accepts an optional `signature` body parameter — a PNG data URL of at most 2 MB, required when the store's `acceptance_mode` is `signature` — and returns `signed_pdf_url`. New error codes `SIGNATURE_REQUIRED` (422), `SIGNATURE_INVALID` (422), `QUOTE_NOT_ORDERABLE` (422), `ALREADY_CONVERTED` (409) and `QUOTE_EXPIRED` (410). See [Quote tokens](/api/orders-and-quotes/quote-tokens).
- New scopes `sales:read` and `sales:write`, accepted anywhere `commerce:read`/`commerce:write` is accepted on the quote-token endpoints. See [Authentication](/api/getting-started/authentication).
- A quote's `status` may now be `draft`: merchant-written quotes exist before they are sent. A draft has no number and is never reachable by token.

### Changed
- Customer endpoints no longer require a module to be enabled; the 403 `Module not enabled: customers` response can no longer occur. See [Customer accounts](/api/customers/index) and [Errors and rate limits](/api/getting-started/errors-and-rate-limits).
- Quote line `quantity` (merchant quotes and the token-addressed quote object) is documented as `number` instead of `integer`: product lines still always report a whole number, but service and material lines can now carry a fractional quantity (hours, metres). Every previously-integer value keeps serializing exactly as before. See [Quote requests](/api/orders-and-quotes/quote-requests) and [Quote tokens](/api/orders-and-quotes/quote-tokens).
- Accept and decline on the quote-token endpoints are throttled to 30 requests per minute, keyed by the caller's IP. See [Errors and rate limits](/api/getting-started/errors-and-rate-limits).

### Notes
- `lines[].quantity` on the quote-token endpoints was always a JSON number and still is; it may now be fractional (two decimals). No field was removed, renamed or retyped.

## 2026-08-25

### Changed
- Public catalog routes (`/v1/api/commerce/products`, `/v1/api/commerce/categories`, `/v1/api/commerce/configurator/*`) now accept an API key carrying **either** the `catalog:read` or the `commerce:read` scope. Existing keys keep working unchanged. See [Authentication](/api/getting-started/authentication).
- New API-key scopes `catalog:read` and `catalog:write` are available to stores with the Catalog module enabled. Scope names for every other module are unchanged.

### Notes
- No request parameter, response field, status value or error envelope changed in this release.
