# Changelog

De storefront API is geversioneerd via het URL-prefix (`/v1`). Binnen v1 is elke wijziging additief: velden, endpoints, optionele parameters en enum-waarden worden alleen toegevoegd. Elke vermelding hieronder noemt de datum, wat er veranderde en welke pagina's van deze referentie zijn bijgewerkt. Dezelfde vermeldingen staan in de changelog van de repository.

## 2026-10-29

### Toegevoegd
- `variants[].image_id` (uuid of null) en `variants[].image` (afbeeldingsobject of null) op `GET /v1/api/commerce/products` en `GET /v1/api/commerce/products/{id}`: de eigen afbeelding van een variant, een van de media-items van het product, dus `image_id` komt overeen met een item in `images` op het detail-endpoint. Producten die uit Shopify zijn geïmporteerd krijgen hem uit de kolom `Variant Image`. Zie [Variantobject](/nl/api/catalog/products#variantobject).

## 2026-10-24

### Opmerkingen
- Documentatiefix, niets veranderd in de API. De eventcatalogus van webhooks documenteert nu de acht abonneerbare sales-events die `WebhookDispatcher` sinds de sales-module toestaat: `quote.offered`, `quote.accepted`, `quote.declined`, `quote.expired` (`{id, number, status, origin, total_amount, currency, email, valid_until, signed_at}`) en `invoice.sent`, `invoice.paid`, `invoice.overdue`, `credit_note.sent` (`{id, number, kind, status, total_amount, paid_amount, open_amount, currency, email, issue_date, due_date}`), met wanneer elk wordt verstuurd. Zie [Webhooks](/nl/api/webhooks/index).
- De scopetabel noemt elke toekenbare scope en de routes die hem controleren: `catalog:read` (en het paar `catalog:write`, `configurator:*`), `contacts:read` / `contacts:write` met `customers:*` als hun vroegere namen, `sales:read` / `sales:write`, en welke scopes toekenbaar zijn maar nog door geen `/v1/api`-route worden gebruikt. Zie [Authenticatie](/nl/api/getting-started/authentication).

## 2026-10-18

### Toegevoegd
- `GET /v1/api/forms/address-lookup` (scope `forms:read`): `country=NL`, `postal_code`, `house_number` en een optionele `addition` geven `200 {street, city, postal_code, lat, lon}` (niet verpakt in `data`). Fouten: `404` (`No address found.`), `422` op `country`, `postal_code` of `house_number`, `503` (`Address lookup is temporarily unavailable.`). 60 requests per minuut per winkel, daarna `429`. Zie [Adres opzoeken](/nl/api/forms/forms#get-v1apiformsaddress-lookup).
- Veldtype `address` bij formulieren. `config` is `{country, lookup?, service_area?}`; het antwoord is `{postal_code, house_number, addition?, street, city, lat?, lon?}` en een ongeldige waarde geeft `Must be a valid address.` op `answers.{key}`. `service_area` is `{origin: {lat, lon, label?}, radius_km, mode: notice | block, message}`. Zie [Veldtypes](/nl/api/forms/index#veldtypes).
- `meta.address` bij een opgeslagen inzending: per `address`-veld met een werkgebied `{distance_km, in_area}`. Bij `mode: "block"` geeft een adres dat aantoonbaar buiten het gebied ligt `422` op `answers.{key}` met de `message` van het veld. Zie [Adresvelden en het werkgebied](/nl/api/forms/submissions#adresvelden-en-het-werkgebied).
- `options_source` bij `choice`-velden: `{object_type, label, description?, sort?, limit?}`. `GET /v1/api/forms/{key}` vult `config.options` bij elk request uit de publiek zichtbare records van de collectie en een inzending wordt daartegen gecontroleerd. Zie [Veldtypes](/nl/api/forms/index#veldtypes).
- Lengtegrenzen voor de onderdelen van een `address`-antwoord, na het verwijderen van de spaties: `postal_code` 12 tekens, `house_number` 20, `addition` 10, `street` en `city` elk 255. Een langer onderdeel geeft `Must be a valid address.` op `answers.{key}`, ook in `check()` van `@borkol/forms`. Zie [Veldtypes](/nl/api/forms/index#veldtypes).
- `@borkol/forms` 0.2.0: de optie `persistKey` bij `BorkolForm` en `useBorkolForm` bewaart het concept van een bezoeker tot 30 dagen in `localStorage`, en `formatAddress`, `normaliseAddress` en `distanceKm` worden geëxporteerd. Zie [Het pakket @borkol/forms](/nl/api/forms/index#het-pakket-borkolforms).

### Gewijzigd
- Een formulier mag nu 150 invoervelden hebben (was 60). `heading` en `paragraph` tellen niet mee. Stappen blijven op 20. Zie [Definitie: stappen en velden](/nl/api/forms/index#definitie-stappen-en-velden).

### Opmerkingen
- `address` is een nieuw veldtype: een renderer die een component kiest op `kind` heeft er een nodig. Behandel een onbekend type als additief, zoals altijd binnen `/v1`.
- Bij `GET /v1/api/forms/{key}` is `config.service_area.message` van een `address`-veld een tekst in de gevraagde taal (met de gebruikelijke terugval), zoals elke andere tekst van de definitie. Zie [Veldtypes](/nl/api/forms/index#veldtypes).

## 2026-10-16

### Toegevoegd
- `code: "module_disabled"` en `module` (de module-key) op de body van de module-disabled-error, zowel bij de `403`- als de `404`-variant. `error` blijft ongewijzigd, dus bestaande checks blijven werken. Zie [Module niet ingeschakeld](/nl/api/getting-started/errors-and-rate-limits#module-niet-ingeschakeld).

## 2026-10-13

### Toegevoegd
- **Formulieren.** Een nieuwe module `forms` en een nieuw hoofdstuk. `GET /v1/api/forms/{key}?locale=` (scope `forms:read`) geeft de gepubliceerde versie van een actief formulier terug, verpakt in `data`: `{key, version, name, steps, settings: {thank_you: {message, redirect_url}}}`, met elke tekst in één taal en de standaardtaal van de winkel als terugval. Stappen en velden hebben `show_if`-regels (`equals`, `not_equals`, `contains`, `filled`, `empty`, één niveau `all` of `any`); de veldtypes zijn `text`, `textarea`, `number`, `email`, `phone`, `date`, `select`, `link`, `choice`, `consent`, `hidden`, `file`, `heading` en `paragraph`. Zie [Formulieren](/nl/api/forms/index) en [Een formulier laden](/nl/api/forms/forms).
- `POST /v1/api/forms/{key}/submissions` (scope `forms:write`) controleert de antwoorden met dezelfde regels tegen de gepubliceerde versie, laat antwoorden op verborgen velden weg, slaat de inzending op en antwoordt `201 {"data": {"number", "thank_you": {message, redirect_url}}}`. Fouten volgen de `422`-envelope met keys `answers.{key}` en `files.{key}`; een `honeypot` met een andere waarde dan een lege string of `null` geeft `201` met `number: null` en slaat niets op. Zie [Inzendingen](/nl/api/forms/submissions).
- Header `X-Borkol-Visitor-Ip` op de formulierroutes: de limiet per uur per formulier (1 tot 100 per bezoeker, standaard 5) telt per bezoekersadres, ook pogingen die met `422` stranden, en antwoordt `429 {"message": "Too many submissions. Try again later."}`. Zonder geldige header wordt het adres van de verbinding gebruikt. Zie [Bezoekers-IP en de limiet per uur](/nl/api/forms/index#bezoekers-ip-en-de-limiet-per-uur).
- Scopes `forms:read` en `forms:write`, en `forms` als module-key in `403 {"error": "Module not enabled: forms"}`. Houd `forms:write`-sleutels op je server. Zie [Authenticatie](/nl/api/getting-started/authentication).
- Webhook-events `form.published` (`{key, version}`) en `form_submission.created` (`{id, number, form_key, status, locale, name, email, created_at}`, nooit de antwoorden). Zie [Webhooks](/nl/api/webhooks/index).
- `POST /v1/api/forms/{key}/uploads` (scope `forms:write`, multipart `field` en `file`) slaat één bestand voor een `file`-veld privé op en antwoordt `201 {"data": {"token", "name", "size"}}`. Het type wordt bepaald aan de hand van de inhoud (`image`: JPEG, PNG, WebP, HEIC/HEIF; `pdf`: PDF), de grootte tegen `max_mb` van het veld (maximaal 20 MB); fouten zijn `422` op `field` of `file`. 30 uploads per formulier per bezoeker per uur, daarna `429 {"message": "Too many uploads. Try again later."}`. Tokens werken één keer, horen bij het formulier en het veld, en verlopen na 24 uur. Zie [Uploads](/nl/api/forms/uploads).
- `files` op `POST /v1/api/forms/{key}/submissions`: veld-key naar een lijst uploadtokens, geclaimd met de inzending; een token dat onbekend is, bij een ander formulier of veld hoort, gebruikt of verlopen is, geeft `422` op `files.{key}` (`This upload is invalid or has expired. Upload the file again.`). Zie [Inzendingen](/nl/api/forms/submissions).
- Cloudflare Turnstile op formulieren: `settings.turnstile_site_key` op `GET /v1/api/forms/{key}` als het formulier Turnstile aan heeft en de winkel beide keys heeft ingesteld, en `turnstile_token` op de inzending, gecontroleerd na de limiet per uur en vóór de antwoorden. Een mislukte controle geeft `422` op `turnstile`; tokens werken één keer, dus reset de widget na elke `422`. Zie [Turnstile](/nl/api/forms/index#turnstile) en [Een formulier laden](/nl/api/forms/forms).
- Het pakket `@borkol/forms`: types, de zichtbaarheids- en antwoordregels (`visible`, `check`, `prefill`) en een headless React-renderer (`@borkol/forms/react`) met `upload`, `turnstileToken`, `onServerErrors` en een `formError`-component. Zie [Het pakket @borkol/forms](/nl/api/forms/index#het-pakket-borkolforms).

### Opmerkingen
- Een formulier met Turnstile aan wordt niet gecontroleerd zolang de winkel niet beide Turnstile-keys heeft ingesteld: `turnstile_site_key` ontbreekt dan en inzendingen worden zonder controle geaccepteerd.

## 2026-10-12

### Toegevoegd
- Filteren op een `tags`-, `record_ref_list`- of `record_ref`-property met `filter[{propertyKey}]`: een tag of record-id die de opgeslagen lijst bevat (een verwijzing matcht op gelijkheid). Zie [Records](/nl/api/objects/records).
- `settings` op elk objecttype van `GET /v1/api/objects/types`: `{publishable, slug?}`, waarbij `slug` `{source, prefixes}` is en alleen aanwezig als het type zijn records adressen geeft. `localized` (boolean) en `config` (object of null) op elke propertydefinitie. Zie [Types](/nl/api/objects/types).
- De propertytypes `richtext`, `datetime`, `media`, `media_list`, `link`, `icon`, `token_variant`, `color`, `email`, `phone`, `tags`, `record_ref`, `record_ref_list` en een `repeater` met één niveau nesting voor objectproperties, naast de bestaande types. Properties kunnen worden gelokaliseerd (niet number, money, boolean, date, datetime, customer, color, media_list, recordverwijzingen, repeater, sections; een `media_list` draagt in plaats daarvan `alt` en `caption` als locale-maps). Zie [Objecten](/nl/api/objects/index).
- `published_at` op elk record, en `slugs` (taaltag naar adres) op de records van een type met adressen, beperkt tot de voor de winkel ingeschakelde talen. Zie [Records](/nl/api/objects/records).
- `GET /v1/api/objects/types/{key}/records/by-slug/{slug}?locale=` haalt één gepubliceerd record op via zijn adres in één taal (`locale` is standaard de standaardtaal van de winkel; `422` op `locale` als die niet is ingeschakeld, `404` als niets zichtbaars matcht). Zie [Records](/nl/api/objects/records).
- Webhook-events `object_record.published`, `object_record.updated`, `object_record.unpublished` en `object_record.deleted`, met de payload `{id, type_key, slugs, published_at}`. Ze gaan alleen af bij echte overgangen; een ingepland record dat zijn tijd bereikt, verstuurt niets. Zie [Webhooks](/nl/api/webhooks/index).
- `GET /v1/api/redirects` accepteert ook `tenant:read` en `objects:read`, en stuurt door naar recordadressen op dezelfde manier als naar producten en categorieën (een gewijzigd recordadres legt alleen een redirect vast als het type voor die taal een niet-leeg adresvoorvoegsel heeft). Zie [Redirects en sitemap](/nl/api/getting-started/seo).
- `include=refs` (of `include[]=refs`) op `GET /v1/api/objects/types/{key}/records`, `GET /v1/api/objects/records/{id}` en het by-slug-endpoint klapt `record_ref`- en `record_ref_list`-waarden uit tot `{id, type_key, values, slugs?, published_at}`, één niveau diep. Doelen die niet publiek zichtbaar zijn vallen weg (`null` voor een verwijzing, uit een lijst verwijderd); onbekende `include`-tokens worden genegeerd. Zie [Records](/nl/api/objects/records#verwijzingen-uitklappen).
- Query-parameter `locale` op `GET /v1/api/objects/types/{key}/records`: gelokaliseerde `filter[...]`, `sort` en `q` gebruiken die taal. Een tag van een ingeschakelde taal is nodig om te werken; elke andere waarde valt terug op de standaardtaal van de winkel. Zie [Records](/nl/api/objects/records).
- Objectrecords in `GET /v1/api/sitemap`: items `{type: "object_record", object_type, locale, path, lastmod, alternates}`, één per publiek zichtbaar record, taal en adres, voor types met een niet-leeg adresvoorvoegsel. Zie [Redirects en sitemap](/nl/api/getting-started/seo).
- `GET /v1/api/business` (scope `tenant:read`) geeft het openbare bedrijfsprofiel: handels- en juridische naam, contactgegevens, adres, registratie- en BTW-nummers, `social_links` (`{platform, url, label}`, 23 platforms), de standaard `locale` en `content_locales`. Bank-, ondertekenaar-, afzender- en voorwaardenvelden worden nooit getoond, en een winkel zonder profiel antwoordt `200` met nulls. Zie [Bedrijfsprofiel](/nl/api/getting-started/business).

### Gewijzigd
- **Mediawaarden zijn objecten.** Een `media`-waarde (een property, of een veld binnen een sectie of repeaterrij) is nu `{id, url, alt, width, height, mime_type}`, of `null` als het item ontbreekt of privé is. Het was het media-id als string. Een `media_list`-waarde is een lijst van dezelfde objecten, elk met een `caption`; items die ontbreken of privé zijn, vallen uit de lijst. Het media-id zelf ophalen is niet meer nodig. Zie [Objecten](/nl/api/objects/index#mediawaarden).
- **Concepten en ingeplande records zijn verborgen.** Op een type met `settings.publishable: true` ontbreken concepten en records met een `published_at` in de toekomst in de recordlijst en geven ze `404` op `GET /v1/api/objects/records/{id}` en via slug. `filter[status]` wordt publiek niet geaccepteerd (`422` op `filter`). Typen zonder publicatiestap blijven ongewijzigd. Zie [Records](/nl/api/objects/records) en [Publiceren en zichtbaarheid](/nl/api/objects/index#publiceren-en-zichtbaarheid).
- **Gelokaliseerde waarden worden teruggesneden tot de ingeschakelde talen.** Een gelokaliseerde property of veld geeft een map van taaltag naar waarde met alleen de voor de winkel ingeschakelde talen. Een opgeslagen lege `sections`-waarde komt terug als `[]`; secties van een type dat niet meer bestaat, vallen weg. Zie [Gelokaliseerde waarden](/nl/api/objects/index#gelokaliseerde-waarden).
- **`GET /v1/api/redirects` en `GET /v1/api/sitemap` worden door het platform geleverd.** Zelfde paden, zelfde responses. Ze vereisen de catalog-module niet meer (geen `403 Module not enabled: catalog` meer), accepteren één van de scopes `catalog:read`, `commerce:read`, `tenant:read` of `objects:read`, en de sitemap lijst alleen de items van ingeschakelde modules (een winkel zonder catalog-module krijgt een lege lijst). De referentiepagina is verplaatst van Catalogus naar Aan de slag. Zie [Redirects en sitemap](/nl/api/getting-started/seo).
- **Gelokaliseerde filters, sortering en zoeken volgen `locale`.** Filteren of sorteren op een gelokaliseerde property, en `q` op een gelokaliseerde tekstproperty, lezen nu de waarde in de gevraagde taal (de standaardtaal van de winkel als `locale` ontbreekt) in plaats van de ruwe opgeslagen map. Zie [Records](/nl/api/objects/records).
- **Vertalingen zonder standaardtaal blijven bewaard.** Een vertaalbare property of veld dat niet verplicht is en geen waarde voor de standaardtaal van de winkel heeft, bijvoorbeeld `{"en": "Dusk"}` met standaard `nl`, wordt zoals ingevuld opgeslagen in plaats van weggelaten; alleen een map waarvan alle waarden leeg zijn telt nog als leeg. Een verplicht veld geeft nog steeds `422`. De `richtext`-allowlist bevat nu ook `img` met alleen `src` en `alt` (`https://` of root-relatieve `/` bronnen; elke andere `img` wordt verwijderd). Zie [Objects](/nl/api/objects/index#gelokaliseerde-waarden).

### Opmerkingen
- De recordvorm kreeg `published_at` (altijd) en `slugs` (typen met adressen) erbij. Behandel onbekende velden als additief, zoals altijd binnen `/v1`.

## 2026-10-11

### Toegevoegd
- `capability` (`standard` of `made_to_measure`) en `capability_active` (boolean) op het productobject van `GET /v1/api/commerce/products` en `GET /v1/api/commerce/products/{id}`. `capability` zegt hoe het product in een cart komt; `capability_active` is `false` zolang de module die het verkoopt (commerce voor `standard`, configurator voor `made_to_measure`) uitgeschakeld is. Zie [Producten](/nl/api/catalog/products).
- `unit` op het productobject (`piece`, `kg`, `g`, `l`, `m`, `m2`, `m3`, `hour`) en op elke orderregel (`lines[].unit`, de eenheid van het product op het moment van de order). Zie [Producten](/nl/api/catalog/products) en [Bestellingen](/nl/api/orders-and-quotes/orders).
- Productvelden `compare_at_amount`, `barcode`, `weight_grams`, `requires_shipping`, `track_stock`, `returnable`, `lead_time_days`, `tags` en `social_image`, en variantvelden `barcode` en `weight_grams`. Zie [Producten](/nl/api/catalog/products).
- `GET /v1/api/redirects?path=` geeft `{data: {from_path, to_path, status}}` terug voor een product- of categoriepad waarvan de slug is gewijzigd (404 als er geen is), en `GET /v1/api/sitemap` lijst elk actief product- en categoriepad op als `{data: [{type, path, lastmod}]}`. Beide nemen `catalog:read` of `commerce:read` en de catalog-module. Zie [Redirects en sitemap](/nl/api/catalog/seo).
- Een `code` op het hoogste niveau op de 422 waarmee een cart-add een regel weigert (`POST /v1/api/commerce/carts/{id}/lines`): `CAPABILITY_INACTIVE`, `INVALID_CONFIGURATION` of `NOT_PURCHASABLE`, naast de ongewijzigde `message` en `errors`. Zie [Cartregels](/nl/api/carts-and-checkout/cart-lines) en [Errors en rate limits](/nl/api/getting-started/errors-and-rate-limits).
- `attributes.returnable` en `attributes.lead_time_days` op elke orderregel die via de checkout of een geaccepteerde offerte is geplaatst; een made-to-measure-regel is nooit retourneerbaar. Zie [Bestellingen](/nl/api/orders-and-quotes/orders).

### Gewijzigd
- **Breaking:** `type` is verwijderd uit het productobject. Gebruik in plaats daarvan `capability`, plus `requires_shipping` en `track_stock`: voormalige `simple`- en `variable`-producten zijn `standard`; `configurable`-producten zijn `made_to_measure` (en niet retourneerbaar); `digital`-, `license`- en `subscription`-producten zijn `standard` met `requires_shipping: false` en `track_stock: false`. Zie [Producten](/nl/api/catalog/products).
- **Breaking:** hoeveelheden zijn decimaal. `quantity` op cart-, order- en offerteregels, `available` bij beschikbaarheid, `out_of_stock[].requested` en `.available` bij checkout en offerte-acceptatie, en `quantity` in de `stock.adjusted`-webhook zijn JSON-getallen die decimalen kunnen hebben (`0.75`, `12`). `quantity` op `POST /v1/api/commerce/carts/{id}/lines` en `PATCH .../lines/{lineId}` accepteert tot drie decimalen; een product met `unit` `piece` neemt nog steeds alleen gehele getallen (422 op `quantity`). Zie [Cartregels](/nl/api/carts-and-checkout/cart-lines), [Carts](/nl/api/carts-and-checkout/carts), [Bestellingen](/nl/api/orders-and-quotes/orders), [Beschikbaarheid](/nl/api/stock/availability) en [Webhooks](/nl/api/webhooks/index).
- **Breaking:** de configurator-endpoints (`GET /v1/api/commerce/configurator/products/{id}/schema`, `POST .../quote`) hangen af van de configurator-module: zonder die module antwoorden ze `403 {"error":"Module not enabled: configurator"}` (was `catalog`). Ze antwoorden 404 voor een product waarvan de `capability` niet `made_to_measure` is. Zie [Configuratieschema](/nl/api/configurator/schema), [Prijsofferte](/nl/api/configurator/quote) en [Hoe de configurator werkt](/nl/api/configurator/index).
- Configurator-dimensies moeten gehele getallen zijn; een waarde met decimalen werd vroeger afgekapt en wordt nu geweigerd. Het quote-endpoint antwoordt 422 op `dimensions.{key}` (`{Label} must be a whole number.`) in zijn gewone validatie-envelope, zonder `code` op het hoogste niveau; een cart-add geeft dezelfde fout met `code` `INVALID_CONFIGURATION`. Zie [Prijsofferte](/nl/api/configurator/quote) en [Cartregels](/nl/api/carts-and-checkout/cart-lines).
- **Breaking:** `components[].qty` in de configuratiesnapshot op cart- en orderregels is een exacte decimale string (`"2.16"`), was een integer. Zie [Carts](/nl/api/carts-and-checkout/carts) en [Bestellingen](/nl/api/orders-and-quotes/orders).
- **Breaking:** `POST /v1/api/commerce/carts/{id}/quote-request` hoort bij de sales-module en antwoordt `404 {"error":"Module not enabled: sales"}` wanneer de winkel sales heeft uitgeschakeld. Quote-triggers gaan alleen af wanneer sales is ingeschakeld en offerteaanvragen aan staan; anders is `requires_quote` altijd false en weigert de checkout nooit met `QUOTE_REQUIRED`. Zie [Offerteaanvragen](/nl/api/orders-and-quotes/quote-requests), [Checkout](/nl/api/carts-and-checkout/checkout) en [Errors en rate limits](/nl/api/getting-started/errors-and-rate-limits).
- `purchasability` wordt ingesteld door de sales-module en is `direct` wanneer de merchant niets heeft ingesteld; het wordt ook gerapporteerd wanneer sales uitgeschakeld is. Een `quote`-product kan aan een cart worden toegevoegd: de cart meldt `requires_quote: true` met reden `product_purchasability`, de checkout weigert met `409 {"code": "QUOTE_REQUIRED", "reasons": [...]}`, en de cart gaat via de offerteaanvraag. De instelling altijd offreren van een contactgroep is nu een sales-instelling; de reden `customer_group` is ongewijzigd. Zie [Producten](/nl/api/catalog/products) en [Offerteaanvragen](/nl/api/orders-and-quotes/quote-requests).
- `stock_status` is `untracked` voor een product met `track_stock: false` en al zijn varianten, ook wanneer er voorraadrijen bestaan, op het productobject en op `GET /v1/api/stock/availability` en `GET /v1/api/stock/products/{productId}` (`tracked: false`). Zie [Producten](/nl/api/catalog/products) en [Beschikbaarheid](/nl/api/stock/availability).
- De checkout vereist `shipping_method_id` wanneer de cart een regel bevat waarvan het product `requires_shipping: true` heeft (was: een product van type `simple`, `variable` of `configurable`). Zie [Checkout](/nl/api/carts-and-checkout/checkout) en [Verzendmethoden](/nl/api/carts-and-checkout/shipping-methods).
- Offerteregels die uit een cart zijn gemaakt hebben `unit` `pcs` voor een product dat per stuk verkocht wordt en anders de eenheid van het product (was `null`). Zie [Offerteaanvragen](/nl/api/orders-and-quotes/quote-requests) en [Offertetokens](/nl/api/orders-and-quotes/quote-tokens).
- Een offerte accepteren in bestelmodus reserveert voorraad via de line handler van elk product tegen de geoffreerde prijs, zodat een made-to-measure-regel zijn componenten afboekt. Alleen een echt tekort weigert met `OUT_OF_STOCK`; een regel waarvan het product niet meer verkocht kan worden, wordt zonder voorraadmutatie omgezet. Zie [Offertetokens](/nl/api/orders-and-quotes/quote-tokens).

### Verouderd
- `QUOTE_NOT_ORDERABLE` (422) op `POST /v1/api/sales/quotes/by-token/{token}/accept` en `POST /v1/api/commerce/quotes/by-token/{token}/accept` wordt niet meer teruggegeven: een offerte met fractionele regelhoeveelheden wordt omgezet in een bestelling met die hoeveelheden. Blijf de code tolereren, maar verwacht hem niet meer te zien. Zie [Offertetokens](/nl/api/orders-and-quotes/quote-tokens).

### Opmerkingen
- Het `/v1`-oppervlak is geherstructureerd voordat een productieconsument ervan afhankelijk was, dus de breaking changes hierboven zitten binnen `/v1` in plaats van onder een nieuwe URL-versie. Vanaf deze release zijn wijzigingen binnen `/v1` weer additief.
- Documentatiecorrecties, er is niets aan de API veranderd: de product- en categorie-endpoints accepteren `catalog:read` of `commerce:read` en hangen af van de catalog-module (de referentie noemde `commerce:read` en `commerce`); `invoice_id` op het orderobject en de volledige offerteaanvraag-respons (`origin`, `intro`, `terms`, `acceptance_mode`, `pdf_url`, `signed_at`, `signed_pdf_url`, en de regelvelden `kind`, `unit`, `description`, `tax_rate`, `unit_price`) zijn nu gedocumenteerd. Zie [Producten](/nl/api/catalog/products), [Categorieën](/nl/api/catalog/categories), [Bestellingen](/nl/api/orders-and-quotes/orders) en [Offerteaanvragen](/nl/api/orders-and-quotes/quote-requests).

## 2026-09-17

### Toegevoegd
- `discount_total` (money) en `discount_breakdown` (array of null) op het orderobject dat `POST /v1/api/commerce/carts/{id}/checkout`, `GET /v1/api/commerce/orders/{id}` en `GET /v1/api/commerce/my/orders` teruggeven. `discount_total.amount` is de productkorting die bij het afrekenen is toegepast (`0` als er geen was); `discount_breakdown` bevat per toegepaste korting één `{discount_id, name, code, kind, amount}`-item en is `null` als er geen korting gold. Beide zijn additief; er is geen bestaand veld gewijzigd. Zie [Orders](/nl/api/orders-and-quotes/orders) en [Checkout](/nl/api/carts-and-checkout/checkout).
- `message` op de `OUT_OF_STOCK`-weigering van `POST /v1/api/commerce/quotes/by-token/{token}/accept` (alleen bestelmodus). De 409-body luidt nu `{"code": "OUT_OF_STOCK", "message": "Insufficient stock.", "out_of_stock": [...]}`; de `out_of_stock`-regels zijn ongewijzigd. Additief, en de envelope komt nu overeen met elke andere weigering op het endpoint (`code` + `message`). Zie [Offertetokens](/nl/api/orders-and-quotes/quote-tokens).

### Gewijzigd
- `POST /v1/api/commerce/carts/{id}/claim` voegt nu **elke** andere `active` cart van de klant samen in de geclaimde cart, niet alleen de eerste die gevonden wordt. Elke samengevoegde cart krijgt status `merged` en geeft zijn voorraadreserveringen vrij, precies zoals voorheen; er blijft alleen geen verweesde actieve cart meer achter. Zie [Een cart claimen](/nl/api/carts-and-checkout/claim) en [Storefront-gids](/nl/api/storefront-guide/index).
- De claim-merge neemt identiteit mee: de geclaimde cart behoudt zijn eigen `email` en `metadata` als die gezet zijn en neemt anders de waarde van de samengevoegde cart over, zodat een guest-e-mailadres dat vóór het inloggen is vastgelegd niet meer verloren gaat. Er is geen veld toegevoegd of van type veranderd op de cart-envelope. Zie [Een cart claimen](/nl/api/carts-and-checkout/claim).

### Opmerkingen
- `POST /v1/api/commerce/carts/{id}/lines`: twee gelijktijdige identieke adds (zelfde product, variant en configuratie) lieten de verliezer voorheen falen met een 500. De verliezer telt zijn aantal nu op bij de winnende regel en geeft dezelfde 201-envelope terug als voorheen, nog steeds begrensd op `max_quantity_per_line`. Zie [Cart-regels](/nl/api/carts-and-checkout/cart-lines).
- De verwerking van `X-Customer-Token` volgt nu consequent de gedocumenteerde regel "moet naar een klant herleiden, anders 401": een token dat voorbij zijn `expires_at` is, of ouder is dan het sessievenster van het platform, herleidt niet meer en kan dus geen klantgebonden cart claimen of afrekenen. Log opnieuw in voor een vers token. Zie [Een cart claimen](/nl/api/carts-and-checkout/claim) en [Checkout](/nl/api/carts-and-checkout/checkout).
- `POST /v1/api/customers/forgot-password` antwoordt nog steeds 202 met dezelfde body voor bekende en onbekende adressen; het doet nu op beide paden ook evenveel queue-werk, zodat de responstijd niet langer verraadt of een account bestaat. Zie [Wachtwoorden](/nl/api/customers/passwords).
- Een `bogo`-korting geeft zijn ontvangaantal nu één keer per kwalificatie in plaats van één keer per cart: het aantal afgeprijsde eenheden is `get_quantity × floor(kwalificerend koopaantal ÷ minimaal koopaantal)`, begrensd door het aantal op de goedkoopste ontvangregel. `discounts[].amount` op de cart en `discount_total` op de order groeien mee; er is geen veld of enum-waarde gewijzigd. Zie [Cart-kortingen](/nl/api/carts-and-checkout/discounts).
- Producten van het type `configurable` worden uitsluitend door de configurator-engine geprijsd; klantgroep- en staffelprijsrijen die na een typewijziging zijn achtergebleven verschijnen niet meer in `price`, `quantity_tiers` of de cart-regelprijs van zo'n product. Simpele producten zijn niet geraakt. Zie [Producten](/nl/api/catalog/products).
- `POST /v1/api/commerce/quotes/by-token/{token}/accept`: de `QUOTE_NOT_OFFERED`-weigering die tijdens het opbouwen van de bestelling ontstaat (een race met een andere accept van dezelfde offerte) draagt nu het gedocumenteerde bericht `Quote is not awaiting a response (status: ...)` in plaats van `Quote cannot be accepted in its current status (...)`. Code en status zijn ongewijzigd. Zie [Offertetokens](/nl/api/orders-and-quotes/quote-tokens).

## 2026-09-04

### Gewijzigd
- Een configurator-keuze waarvan de `requires`-regel niet vervuld kan worden door de andere gepubliceerde keuzes wordt nu weggelaten uit `GET /v1/api/commerce/configurator/products/{id}/schema`, in plaats van gepubliceerd te worden en daarna bij het opvragen van een quote afgewezen te worden. Het filteren werkt door, dus elke keuze die het schema toont kan geselecteerd en geprijsd worden. Zie [Configuratieschema](/nl/api/configurator/schema) en [Hoe de configurator werkt](/nl/api/configurator/index).
- `POST /v1/api/commerce/configurator/products/{id}/quote` past dezelfde beschikbaarheidsregel toe: een keuze selecteren die het schema niet meer publiceert faalt met het bestaande generieke `choices`-bericht, en de `choices.{choiceId}`-berichten noemen nu alleen nog keuzes die beschikbaar zijn, zodat een gedeactiveerde keuze nooit in een foutmelding wordt prijsgegeven. Zie [Prijsofferte](/nl/api/configurator/quote).
- Een prijsmatrix met een condition op een niet-beschikbare keuze wordt nu weggelaten uit `price_matrices` in plaats van vermeld te worden met een ingekorte `conditions`-array. Zo'n matrix kan nooit in aanmerking komen, dus de engine beschouwt de configuratie als `not_priceable` in plaats van erop terug te vallen. Zie [Configuratieschema](/nl/api/configurator/schema).

### Opmerkingen
- Documentatiecorrectie op beide configurator-endpoints: ze accepteren een API key met **ofwel** de scope `catalog:read` ofwel `commerce:read` (ongewijzigd sinds 2026-08-25), en hun module-gate is de catalog-module, dus de 403-body luidt `{"error":"Module not enabled: catalog"}`. De referentie noemde voor beide `commerce:read` en `commerce`. Er is niets aan de API veranderd. Zie [Prijsofferte](/nl/api/configurator/quote), [Configuratieschema](/nl/api/configurator/schema) en [Hoe de configurator werkt](/nl/api/configurator/index).

## 2026-08-26

### Toegevoegd
- Customer-endpoints worden nu ook aangeboden op `/v1/api/contacts/*`; de eerdere `/v1/api/customers/*` paden blijven bestaan als permanente alias met identieke request- en responsevormen. Nieuwe API-key scopes `contacts:read` en `contacts:write` zijn beschikbaar; bestaande `customers:read` / `customers:write` keys blijven ongewijzigd werken. Zie [Contactaccounts](/nl/api/customers/index).
- Het contactprofiel (`GET/PATCH /v1/api/contacts/me`, ook `.../customers/me`) bevat nu `kind` (`person` of `company`), `company_name`, `registration_number`, `vat_id`, `external_id`, en `display_name`. Deze velden zijn alleen-lezen via de storefront API. `email` kan nu `null` zijn voor elk door de handelaar aangemaakt contact; registratie via de storefront vereist het nog steeds. Zie [Profiel](/nl/api/customers/profile).
- Het veld `type` van een adres accepteert een nieuwe waarde, `work`, naast `shipping` en `billing`. Zie [Adressen](/nl/api/customers/addresses).
- `contact_id` op bestelling- en merchant-offerteobjecten: dezelfde waarde als `customer_id`, onder de canonieke naam vanaf nu. `customer_id` verandert niet en blijft bestaan. Het token-geadresseerde offerteobject bevat geen van beide. Zie [Bestellingen](/nl/api/orders-and-quotes/orders) en [Checkout](/nl/api/carts-and-checkout/checkout).
- De offertetoken-endpoints worden nu ook aangeboden op `/v1/api/sales/quotes/by-token/{token}` (`GET`, `/accept`, `/decline`). `/v1/api/commerce/quotes/by-token/*` blijft identiek en permanent werken; beide paden accepteren een key met `sales:read`/`sales:write` **of** `commerce:read`/`commerce:write`. Zie [Offertetokens](/nl/api/orders-and-quotes/quote-tokens).
- `GET /v1/api/{sales,commerce}/quotes/by-token/{token}` retourneert `origin`, `intro`, `terms`, `scope_items[]`, `acceptance_mode`, `pdf_url`, `signed_at` en `signed_pdf_url`; elk item van `lines[]` krijgt er `kind`, `unit`, `description` en `tax_rate` bij. Zie [Offertetokens](/nl/api/orders-and-quotes/quote-tokens).
- `POST /v1/api/{sales,commerce}/quotes/by-token/{token}/accept` accepteert een optionele body-parameter `signature` — een PNG-data-URL van hoogstens 2 MB, verplicht wanneer de `acceptance_mode` van de winkel `signature` is — en retourneert `signed_pdf_url`. Nieuwe foutcodes `SIGNATURE_REQUIRED` (422), `SIGNATURE_INVALID` (422), `QUOTE_NOT_ORDERABLE` (422), `ALREADY_CONVERTED` (409) en `QUOTE_EXPIRED` (410). Zie [Offertetokens](/nl/api/orders-and-quotes/quote-tokens).
- Nieuwe scopes `sales:read` en `sales:write`, overal geaccepteerd waar `commerce:read`/`commerce:write` wordt geaccepteerd op de offertetoken-endpoints. Zie [Authenticatie](/nl/api/getting-started/authentication).
- De `status` van een offerte kan nu `draft` zijn: door de merchant geschreven offertes bestaan voordat ze worden verstuurd. Een draft heeft geen nummer en is nooit bereikbaar via token.

### Gewijzigd
- Customer-endpoints vereisen niet langer dat een module is ingeschakeld; het 403-antwoord `Module not enabled: customers` kan niet meer voorkomen. Zie [Klantaccounts](/nl/api/customers/index) en [Errors en rate limits](/nl/api/getting-started/errors-and-rate-limits).
- Offerteregel `quantity` (merchant-offertes en het token-geadresseerde offerteobject) wordt nu gedocumenteerd als `number` in plaats van `integer`: productregels melden nog steeds altijd een heel getal, maar dienst- en materiaalregels kunnen nu een fractionele hoeveelheid bevatten (uren, meters). Elke voorheen geheeltallige waarde blijft exact hetzelfde serialiseren. Zie [Offerteaanvragen](/nl/api/orders-and-quotes/quote-requests) en [Offerte-tokens](/nl/api/orders-and-quotes/quote-tokens).
- Accept en decline op de offertetoken-endpoints zijn beperkt tot 30 requests per minuut, gesleuteld op het IP-adres van de aanroeper. Zie [Errors en rate limits](/nl/api/getting-started/errors-and-rate-limits).

### Opmerkingen
- `lines[].quantity` op de offertetoken-endpoints was altijd al een JSON-getal en is dat nog steeds; het mag nu fractioneel zijn (twee decimalen). Er is geen veld verwijderd, hernoemd of van type veranderd.

## 2026-08-25

### Gewijzigd
- Publieke catalogusroutes (`/v1/api/commerce/products`, `/v1/api/commerce/categories`, `/v1/api/commerce/configurator/*`) accepteren nu een API key met **ofwel** de scope `catalog:read` ofwel `commerce:read`. Bestaande keys blijven ongewijzigd werken. Zie [Authenticatie](/nl/api/getting-started/authentication).
- Nieuwe API-key scopes `catalog:read` en `catalog:write` zijn beschikbaar voor winkels met de Catalog-module ingeschakeld. Scope-namen van alle andere modules zijn ongewijzigd.

### Opmerkingen
- In deze release is geen requestparameter, responseveld, statuswaarde of error envelope gewijzigd.
