borkoldocs

Getting Started

Changelog

Dated, newest-first record of every change to the public storefront API. Read this before upgrading an integration.

View as Markdown

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.
  • 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 and 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.
  • 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.
  • 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 and 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.

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.
  • 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, Carts, Orders, Availability and Webhooks.
  • 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, Price quote and How the configurator works.
  • 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 and 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 and 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, Checkout and 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 and 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 and 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 and 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 and 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.

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.

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, Categories, Orders and 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 and 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.

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 and Storefront guide.
  • 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.

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.
  • 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 and 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.
  • 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.
  • 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.
  • 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.

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 and How the configurator works.
  • 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.
  • 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.

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, Configuration schema and How the configurator works.

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.
  • 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.
  • The address type field accepts a new value, work, alongside shipping and billing. See 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 and 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.
  • 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.
  • 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.
  • New scopes sales:read and sales:write, accepted anywhere commerce:read/commerce:write is accepted on the quote-token endpoints. See 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 and 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 and 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.

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.
  • 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.