Skip to main content

Changelog

All platform changes in one place. Filter by component or type to find what matters to you.

Release notes for the Storefront SDK packages and boilerplates are in their own changelogs on GitHub. See Versions and release notes.

Component
Type

Sessions and carts no longer drop out early

Expired access tokens are now renewed correctly, also for requests a page makes in the background, so shoppers stay signed in and a cart left open for an hour keeps working and keeps its contents. After signing in, shoppers return to the page they asked for, including deep links with a query string on closed portals and sign-ins from the header form.

Checkout, catalog and search fixes

Checkout asks for a payment method before the payment step, and the pickup checkout shows its payment methods as choices. Products that belong to the catalog through a second category now open instead of returning page not found, and a search for a code with a slash, such as 1403250/A, finds the product. Empty cross-sell blocks and a category name repeated at the end of the breadcrumb are gone.

Security fixes for account pages and sign-in

In version 1.2.1, account pages without a page type setting and the purchase authorization requests page send signed-out visitors to the login page, like the rest of the account area. The address a shopper returns to after signing in must now be on the site's own domain. Update to version 1.2.1.

Choose which event actions an order or quote send runs

triggerOrderSendConfirm, triggerQuoteSend, triggerQuoteSendRequest and triggerQuoteSendValidation take eventActionConfigIds to run exactly those event actions, for example an alternative email or a specific webhook. Event actions have a new isDefault flag: a send without ids runs only the default ones, and every existing event action is a default. orderEventActions lists the event actions a send can target.

Orders take a supplier of up to 50 characters

A product's supplier can hold 50 characters, but an order line took only 35, so a cart or tender with such a product could not become an order. Order and tender lines now take a supplier of up to 50 characters, and the GraphQL product mutations accept 50 as well.

Slug lookups ignore tracking parameters

Looking up a product, cluster or category by slug, and filtering a search on slugs, now ignores everything from the first ? or #. A slug that still carries tracking parameters, such as ?gclid=..., then finds the item instead of answering that it does not exist.

Shipments check that their items belong to the order

Creating or updating a shipment now refuses an order line of another order (SHIPMENT_ITEM_ORDER_MISMATCH_ERROR) and a line that is not a product, such as a surcharge (SHIPMENT_ITEM_NOT_SHIPPABLE_ERROR). Shipping more than was ordered is still accepted, with an OVER_SHIPPED warning in the new warnings field of the result. A shipment with items can no longer move to another order, and a missing or malformed id answers 400 or 404 instead of 500.

Search updates are not held up by imports on other tenants

How quickly product, price and stock changes become searchable no longer depends on large imports running on other tenants.

Product search date filters accept every ISO 8601 date

A createdAt or lastModifiedAt filter with a date only, a date-time with an offset or a date-time without a zone failed the whole product search with PRODUCT_SEARCH_ERROR. These dates are now accepted, and a date-time without a zone is read as UTC. A date that does not exist, such as 2024-02-30, is refused with INVALID_DATE_FILTER_ERROR, which names the field.

Order revisions are not restored once an order has shipments

orderRevisionRestore now refuses an order that has shipments, canceled ones included, with ORDER_REVISION_INVALID_RESTORE_ERROR, because shipments record what was actually sent. Before, restoring a revision that held shipment items failed with a database error.

Choose the document template for an order or quote PDF

orderGetPDF and quoteGetPDF take an optional templateId to render the PDF with another document template than the default one. orderPdfTemplates lists the templates you can choose and marks the defaults, and an unknown id answers DOCUMENT_TEMPLATE_NOT_FOUND_ERROR.

Bulk media endpoints ignore unknown fields and are fully documented

The REST API reference now documents POST /v2/media/videos/bulk, which creates up to 100 videos per request. The uploadImages example of the bulk image endpoint now shows the fields it takes. Both bulk media endpoints now ignore body fields they do not declare, as the other REST endpoints do since 21 October.

Spare parts machines keep the slugs you send

machineCreate and machineUpsert now store a supplied slug exactly as sent instead of always generating slugs from the name. The new useStandardSlugs option generates missing slugs the way product slugs are made. Slugs no longer have to be unique: when several machines share a slug in a language, a slug lookup returns the oldest.

Inactive REST API keys are refused

A REST API key set to inactive kept working. Requests with it are now refused with 401, and a restApiKeyUpdate that leaves out active, such as a rename, no longer makes an inactive key active again.

Guided selling: an AI product advisor for your storefront (beta)

Guided selling is in beta: you can use it now, but its functionality can still change. It asks a customer a few questions and advises up to three products or configured clusters from a catalog you set up, each with the reason it fits. A storefront runs a published flow with the guidedSellingStep mutation; see Guided selling to set up a flow.

Order lines with a storefront price keep their cost price

Bundle lines and order lines whose price the storefront sends (price mode EXTERNAL) were stored with a cost price of 0. New lines now take their cost price from pricing, and lines already on an order keep the one they have.

Deleted products no longer come back in product search

A product that was updated and then deleted within a few seconds could stay in product search. It is now removed in that case too.

Business rules with a date condition can be opened

A business rule with a before or after date condition could not be opened in the Backoffice, because queries that selected the date answered null with a DateTime error. The date is now returned, also in nested conditions.

Attribute filters by name work with many attributes

On a tenant with more than 200 attribute descriptions, a product search that filtered on an attribute by name returned no results unless the query also asked for filters. Text and range filters by name now find the attribute however many attributes you have.

Business rule conditions handle quotes and nested date conditions

A condition value that contains a quote no longer breaks the rule it is in, and a date condition inside an order item condition is now applied instead of being dropped.

REST endpoints ignore body fields they do not recognize

The REST API now removes a request body field that an endpoint does not declare and handles the request as if the field had not been sent. Before, the product, category, cluster, company, contact, customer and order endpoints answered such a field with 400 and property <name> should not exist. A misspelled field therefore has no effect, so check field names against the REST API reference.

REST API reference shows request examples that pass validation

The request examples for products, categories, cluster options, attributes, attribute descriptions, favorite lists, contact companies, bulk contacts, order updates and shipment searches are now accepted as they are shown. Create surcharge also marks taxCodeValue as required.

Product search breaks ties by priority and sorts on short name

Search results with equal sort values are now ordered by priority (highest first) and then by creation date (oldest first), so they keep their order from one page to the next. With CATEGORY_ORDER, products that only match through a subcategory now follow the products ordered in the category, in both sort directions. Sorting on SHORT_NAME now orders results by their short name, with results without one last.

Prices without a calculation base no longer win

A COST_PRICE_PLUS price without a usable cost price, or a LIST_PRICE_MIN price on a list price of 0, was calculated as 0.00 and, being the cheapest, became the customer's price. Such prices are now skipped. A product's bulk price tiers also show a COST_PRICE_PLUS tier from the quantity its cost price starts at, matching the price that is calculated.

Price calculation reports a Company the Contact does not belong to

priceCalculate, priceExplain and priceCalculateBulk answer with PRICE_CALCULATION_INVALID_COMPANY_ERROR when companyId names a Company the Contact does not belong to. Before, this surfaced as INTERNAL_SERVER_ERROR.

Use order and order line attributes in business rule conditions

Business rule conditions can match attributes of the order and, inside an order item condition, attributes of the order line. Use an attribute expression on order.orderAttributes.items or on #.orderItemAttributes.items; businessRuleFieldDefinitions lists both when you pass includeAttributeExpressions: true.

Inactive GraphQL API keys are refused

A GraphQL API key set to inactive kept working with all its roles. It is now refused with 401, and an update that leaves out active, such as a rename, no longer makes an inactive key active again.

Requests whose token fails authentication are refused

The GraphQL API now answers a request whose token cannot be verified with 401 and the error code UNAUTHENTICATED, and a request whose token belongs to another tenant with 403 and FORBIDDEN. Before, such a request was handled as an anonymous one. Send no Authorization header for anonymous requests, and renew an access token before it expires.

Make products exclusive to selected customers

Products and clusters can be marked exclusive: they are hidden from everyone until an orderlist of the new type EXCLUSIVE grants them. Unlike a POSITIVE orderlist, an EXCLUSIVE orderlist only adds products, so the rest of the catalog stays visible. A product in a cluster follows its cluster, and product searches take an exclusive filter to list only exclusive products or to leave them out.

Default order and quote documents show exact tax rates

The default order and quote PDF templates show the exact tax rate on each line and in the VAT summary, for example 8.1% instead of 8%. New tenants get the corrected templates; an existing tenant keeps its own, which can read percentageDecimal instead of the rounded percentage to show the exact rate.

Base a pricesheet discount on a custom cost price

The pricesheet discount type CUSTOM_COST_PRICE_PLUS adds a percentage to a cost price set on the discount itself, in costPrice, instead of the product's own cost price. It applies to product discounts only, and costPrice is required for this type and not allowed for the others. The pricesheet discount CSV import has a matching COST_PRICE column.

CSV imports read the whole uploaded file

CSV imports of products, attributes, prices, inventory and users sometimes failed with an unknown error because the uploaded file was read as empty. The whole file is now read, and a file that is empty is reported as empty.

Clusters sort and filter by price in product search

In product search, a cluster now takes the price of its default product. A price sort places clusters among the products instead of after every priced product, and a price filter no longer drops them. The minPrice and maxPrice of the results include them as well.

Orders keep the language set on the cart

An order now takes its language from cartProcess, otherwise from the language set with cartStart or cartUpdate, otherwise from the primary language of the Contact or Customer. Before, a cart started in French by a Contact whose primary language is German became a German order. cartUpdate with language: null clears the cart's language.

Contacts and Customers without a login get their access updated

A Contact or Customer created with an empty login did not get updated access rights, so a product search for one of the Contact's own companies was refused. An empty login is now stored as no login, and these users are updated like all others.

Attribute values can be written back as they were read

An attribute value sent back in the shape the API returns it, with null in the unused value fields such as decimalValue: null next to intValue: 7, was refused with the message Bad Request Exception. It is now accepted, and an attribute value that is invalid gets an error that names the problem.

Applying incentives in the Sales Hub removes item discounts that no longer apply

When incentives were applied again to a quote or order in the Sales Hub, an item discount from a rule that no longer matched stayed on its line and was charged. Applying incentives now clears the item discounts first, so only rules that still match leave a discount.

Order endpoints name the fields they do not recognize

The order endpoints of the REST API now answer 400 and name the field when a request body contains a field they do not recognize, for example property statuses should not exist. Before, such fields were ignored, so a misspelled filter on POST /v2/orders/search returned every order. Send only the fields listed in the REST API reference.

Load the lines of an order one page at a time

Order.itemsPaginated returns the lines of an order in pages of at most 10, ordered by priority and then id, with itemsFound for the number of matching lines. It filters on order item ids, product ids, SKUs, classes and parent order item ids, so you can count an order's lines or load only its product lines without loading every line.

Order item conditions set through the API now match

incentiveRuleSetOrderItemConditions now sets the order.items path on the condition the rule is evaluated on. Before, a condition sent without that path saved without an error but never matched, so the rule never applied. Rules built in the Backoffice were not affected.

The incentive action enum lists exactly the actions that run

The GraphQL enum of incentive actions is renamed from IncentiveActions to IncentiveAction and documents every action. It now lists exactly the actions carts and the Sales Hub execute: ADD_CHEAPEST_TO_BONUS_ITEMS can now be set, and MOVE_PRODUCT_TO_BONUS_ITEMS and SET_SHIPPING_TAXCODE, which had no effect, are no longer accepted. Operations that pass the action in IncentiveRuleSetActionInput are unaffected; an operation that declares a variable of the enum type itself uses the new name.

Add copies of products as bonus items

Two incentive actions add bonus items that are free unless the rule sets a discount. DUPLICATE_PRODUCT_IN_BONUS_ITEMS adds the product named in the rule, for example for a "buy X, get X free" promotion. DUPLICATE_CHEAPEST_IN_BONUS_ITEMS adds the rule's quantity as copies of the cheapest matching pieces, starting with the cheapest order line, even when the customer ordered fewer.

Company ID conditions in business rules now match

A business rule condition on Company ID compared the ID as text and never matched. Company ID is now a number field, so a rule for a specific Company applies.

Give a discount to the cheapest items in a promotion

The GIVE_DISCOUNT_TO_CHEAPEST_ITEM incentive action discounts as many of the cheapest matching pieces as the rule's quantity, starting with the cheapest order line and continuing on the next line when one runs out. With repeat set to Y, it does this for every complete set, so a three-for-two promotion makes one piece free for every three ordered. The discounted pieces stay on their own order lines instead of being added as a new line.

Switch business rules off and schedule them

Business rules have an active flag, a validity window with validFrom and validUntil (both inclusive) and a localized description. Set them with businessRuleCreate, incentiveRuleCreate or businessRuleUpdate; businessRules can be filtered on active. Carts and the Sales Hub skip a rule that is inactive or outside its window, so a scheduled rule starts and stops without being saved again.

Save a business rule's decision table in one call

businessRuleDecisionTableSave replaces the hit policy, columns, rows and cells of a rule's decision table in one write. For incentive rules, incentiveRuleSave replaces all rows at once and builds the table from rows that each name their conditions, order item condition, action and params. Both save everything or nothing.

Match attributes by name and value in business rule conditions

A new attribute condition matches an attribute by attributeName, an optional language, an operator and a string or number. The value is compared with whatever the attribute stores (text, enum, integer or decimal), so you no longer build a nested condition with separate steps for the attribute and its value. businessRuleFieldDefinitions lists the attribute fields per scope when you pass includeAttributeExpressions: true.

Creating a contact with attributes no longer fails

contactCreate with attributes saved the contact but then returned an error, and the attributes were not stored. That contact could not get a magic token either: magicTokenCreate answered USER_NOT_FOUND_ERROR. The mutation now stores the attributes and completes, and the new contact can get a magic token.

Orderlist filtering for logged-out visitors

Version 1.2.0 sends the configured anonymous user with category, listing and search requests, so a logged-out visitor sees the catalog filtered by Orderlists, as a signed-in user does.

Contact order history, tax flag and Pages tab fixes

A Contact's order, quote and dashboard lists now show only the orders and quotes they placed within their active Company, instead of those of all their colleagues. The intra-community supply flag is set when both the delivery and the invoice address are in another EU country, saving the Pages tab no longer changes a page's type and the SSO login page resolves on a fresh install.

Security fixes in the WordPress plugin

Version 1.1.0 verifies SSO sign-in tokens before it logs a visitor in, closes a gap in the translations editor that gave access to files outside its folder and keeps credentials out of the error log. Plugin cookies now carry the HttpOnly, SameSite and Secure attributes, and cXML PunchOut logs moved to the protected uploads folder. Update to version 1.1.0 or later.

WordPress plugin ready for the 1 March 2027 API removals

Version 1.1.0 stops using the deprecated GraphQL fields and arguments that the API removes on 1 March 2027 (see Deprecations). It reads their replacements instead, such as type for class and createdAt for Order.date, mostly under the old response keys, so template overrides keep working. Update to version 1.1.0 or later before 1 March 2027.

Pickup orders, intra-community supply and order confirmation fixes

Choosing a pickup location now puts the pickup address and its warehouse on the order, and a guest cart is no longer lost when the shopper logs in. The intra-community supply flag is decided on the server from both the delivery and the invoice country, so shops outside the Netherlands no longer flag domestic deliveries. A failed order confirmation is reported as a failure instead of a success.

Full-page cache support, sitemap choices and log rotation

Three new filters let a full-page cache store pages without per-visitor data such as the mini cart and recently viewed products, and product views are recorded after the page loads, so product pages can be cached. The Behavior tab lets you choose which sitemaps to generate: products, categories and brands. The error log now rotates by size and removes old files after a retention period you set.

EU legal guarantee notice, price tier cards and a stock filter

Version 1.1.0 adds a setting that shows the mandatory European legal guarantee notice on product, cluster and checkout pages, in six languages. Bulk prices can also show as selectable tier cards, which can take their tiers from cost prices without showing buy-in prices, and catalog listings get a stock filter.

Catalog, quick order and SEO fixes

Quick order uploads keep the first product of a file without the template's header row, and order references such as 00123 keep their leading zeros. Attribute values no longer disappear when a language lacks a translation, and valueset texts show in the right language. Pages print each hreflang tag once, the product sitemap includes lastmod dates and storefront pages no longer fill debug.log with warnings.

Ticket errors no longer include database details

When a ticket operation failed, for example ticketCreate with an assignedToAdminUserId that does not exist, the error included the database query and its values. The error now names the field and value that caused the problem.

Template render mutations no longer fail on existing templates

templateRenderToHTML, documentTemplateRenderToPDF and emailTemplateRenderAndSend failed with an internal error for every existing template. They now render it with the data you send: the keys of input.data are available under data, so { "name": "World" } fills {{data.name}}. A payload that is already wrapped in a data key renders the same way.

REST API reference shows the correct types and examples

The REST API reference now shows the correct property types and request examples across eleven APIs, including the product, pricing, order and inventory APIs and the company, contact and customer APIs. For example, the order search example uses the real filters status, type, userId, lastModifiedAt, price and sortInputs, where it used fields the endpoint ignores.

Keyword search matches each word and finds clusters by their products' keywords

Product search matches keywords and custom keywords word by word, regardless of case, so it finds keywords that hold several words or are separated by spaces. Every word of the search term must match a whole word of a keyword, not part of one. Clusters are found by the keywords of their products and stay up to date when one of those products changes or is deleted.

Set the number of price decimals per tenant

A new tenant setting, priceDecimals, holds the number of decimals used to show and round prices, from 2 to 5, with 2 as the default. Read it with the tenant query to show and round prices in your frontend the same way, and change it with tenantUpdate. Both need a key with a configuration role.

See whether an order is not, partially or fully shipped

Orders and tenders have a new fulfillmentState field in GraphQL, with the value NONE, PARTIAL or FULL. It compares the ordered quantity of each product line with the quantities on dispatched shipments, so surcharge and incentive lines are not counted. Order search in GraphQL and REST accepts a fulfillmentStates filter that returns only the orders in the states you choose.

Order created events carry the order's source and account manager

The ORDER_CREATED event now sends the order as it was stored, so source and accountManagerId match what the API returns for the order. Before, the event could show webshop as the source and leave the account manager empty. cartProcess accepts source (up to 50 characters, webshop when left out) and accountManagerId, which are stored on the order and included in the event.

Incentive rule item conditions reject expressions they cannot store

incentiveRuleSetOrderItemConditions failed with BUSINESS_RULE_DECISION_TABLE_UPDATE_ERROR for any condition that was not a complex expression, and a complex expression without sub-expressions failed the same way, also in businessRuleDecisionTableSetCell. Order item conditions now need at least one expression, all of them complex, and other input is rejected with BAD_REQUEST. A complex expression without sub-expressions is saved and applies to every item.

Deprecation notices say what to use instead

Deprecated fields in the GraphQL schema and the REST specs are marked as deprecated and say what replaces them, or that they can be dropped. logout no longer points to a signOut mutation, which does not exist: it has no effect and always returns null, so remove the call and clear the tokens on your side.

Use taxPercentageDecimal for tax rates with decimals

A tax rate with decimals, such as 8.1%, made cart, tender and order queries fail with Int cannot represent non-integer value. New taxPercentageDecimal fields, and percentageDecimal on order tax totals, return the exact rate, while taxPercentage and percentage are deprecated and return the rate rounded to a whole number. When you create or update order lines, taxPercentage is optional and taxPercentageDecimal accepts a rate with decimals.

Write notes on contacts

contactCreate, contactUpdate, contactRegister and contactCreateAccount accept notes, and so does customerCreateAccount, which uses the same input. Leave notes out to keep the current value, or send null to clear it.

Prices for a company alone no longer include the anonymous user's pricesheets

When a price was calculated for a company without a contact or customer, for example with priceCalculate or priceExplain, the pricesheets of the channel's anonymous user were added to those of the company. The price now uses only the pricesheets of the company.

Attribute filters keep finding clusters after a change

After a cluster or one of its products changed, the attribute values of the cluster were stored without their type, so filtering on a product attribute no longer found the cluster until the catalog was fully reindexed. Clusters now keep their attribute values and types when they change.

Category product searches return products that belong to a cluster

A product search within a category (Category.products) with class: PRODUCT returned an empty page when all matching products belonged to clusters, while itemsFound still counted them. Those products are now returned.

Placing products and clusters in categories requires a product role

The mutations that add, remove, move and order products and clusters in categories, such as productBulkMove and clusterBulkMove, now require a key with the product owner or editor role. productCsvImport requires the product owner role. Before, these mutations did not check the roles of the caller.

Cart search returns at most 100 carts per page

Cart search accepts a page size (offset) from 1 to 100 and a page of 1 or more, in GraphQL and REST. A larger page size used to run until the request timed out and is now rejected with a validation error, so request more pages instead.

Match one of several values in a single business rule condition

Array conditions in business rules have two new operators: containsAny matches when the array holds at least one of the given values, and containsNone when it holds none of them. Pass the values as a comma-separated list in string, or a single value in number, instead of repeating a condition for every value.

Only order attribute descriptions are protected from deletion while in use

Deleting an attribute description was refused with ATTRIBUTE_DESCRIPTION_IN_USE as soon as any attribute value used it, but now only order and order item descriptions stay protected while they are in use. Descriptions for products, clusters, categories, companies, contacts and customers are deleted together with their attribute values, in single and bulk deletes. Deletes are also much faster.

Signed-in users can only look up and change their own login account

authentication, authenticationDelete and claimsReset now only accept the email address or user ID of the signed-in user, while keys with the matching user role keep access to every account. Before, any signed-in user could use them on any account in the tenant.

Retry checkout without creating a second order

Calling cartProcess again for a cart whose items have not changed since it was processed returns the order it already created instead of placing a second one, so a checkout that timed out can be retried safely. When pricing is temporarily unavailable, cart calls fail with PRICING_UNAVAILABLE_ERROR, which you can retry after a short wait, while FETCH_PRICING_ERROR now only means that a line cannot be priced.

Find products that are not in any category

Product search treats categoryId: 0 as a filter for the products and clusters that are not in any category. Before, a 0 was ignored and the search returned the same results as one without a category filter. If you send 0 to mean all categories, leave categoryId out instead.

See whether product filters cover all your attributes

Product filters are built from at most 200 attribute descriptions, so with more searchable attributes than that the filters list can come back incomplete or even empty. The new filterCoverage field next to filters shows how many attributes matched and whether the limit left any out. To get filters for specific attributes regardless of the limit, pass their IDs in the ids input of filters.

Replace or clear pricesheet assignments in one update

pricesheetUpdate and PATCH /v2/pricesheets/{id} accept the assignment fields companyIds, contactIds, customerIds and usergroups, plus the source variants companySources, contactSources and customerSources. A field you send replaces the current assignments of that type and an empty array clears them, while a field you leave out keeps its assignments. To add or remove single assignments, keep using pricesheetAssign and pricesheetUnassign.

Calculate prices for many products in one request

The new priceCalculateBulk query prices up to 5,000 combinations of product and quantity in one request, for one Contact, Customer or Company in one tax zone. Each line comes back with either a price or an error, so a product that cannot be priced does not fail the others. Use it instead of many separate priceCalculate calls, for example to price a quick order form.

Faster order search that matches each word of the term

Order search now splits term into words and returns the orders where every word matches one of the searched fields, where before the whole term had to match as one phrase. Words need 3 to 255 characters and only the first five count, while a number of any length matches the order ID exactly instead of as part of it. A term without any usable word, such as ab, is now rejected with a validation error instead of returning results.

Fetch several orders by ID in one request

Order search takes an ids filter in both APIs, so you can fetch orders you already know in one request instead of one request per order. IDs that do not exist or that you cannot access are left out of the result instead of failing the request.

Larger requests for carts, orders and stock updates

Requests for carts, checkout, orders, stock, surcharges and favorite lists were rejected once their body passed 100 KB, so orders with many lines or large stock updates could fail, sometimes with an unclear error. The limit is now 1 MB, or 3 MB for orders.

Inventory delivery dates include the time zone

The nextDeliveryDate of an inventory record now comes back as a full UTC timestamp, for example 2026-04-01T00:00:00.000Z. Before, the time zone was left off (2026-04-01T00:00:00), so it could be read as local time instead of UTC.

Track allocated and expected stock per inventory record

Inventory records take two new quantities from your ERP: allocatedQuantity for stock that is sold but not yet shipped and expectedQuantity for stock that is purchased but not yet received. From them each record derives freeQuantity (the stock you can still promise) and economicQuantity, while a product's inventory adds totals for all four plus nextExpectedDeliveryDate for stock that is actually on its way. A quantity your system does not report stays null instead of counting as 0, so the derived figures only use numbers you actually sent.

Large carts recalculate faster

The prices of all lines in a cart are now calculated together, so adding items to a cart with many lines and checking it out is faster. Product queries that select surcharges for many products at once are faster too.

Carts can hold more than 1,000 lines

Adding items to a cart failed once it held about 1,000 lines, with the exact number differing per tenant. Carts can now grow past that and orders of that size can be opened again for editing.

Cart changes that fail are rolled back

When cartItemBulk, cartUpdateItem or cartDeleteItem fails because the cart cannot be recalculated, the change is undone and the cart stays as it was. Before, lines from a failed cartItemBulk call stayed in the cart without showing up and made every later change fail. A failed cartItemBulk call lists the lines it undid in extensions.rolledBackItemIds, while an itemId the cart does not hold is rejected with CART_ITEM_NOT_FOUND_ERROR before anything is written.

Delete products, categories, companies, contacts and addresses in bulk

New mutations delete many records in one request: productDeleteBulk, categoryDeleteBulk, companyDeleteBulk, contactDeleteBulk, companyAddressDeleteBulk and customerAddressDeleteBulk, with matching bulk DELETE endpoints in the REST API. A record that cannot be deleted does not stop the rest of the batch: the response lists it with the reason, for example a Company that still has Contacts.

Search products as the anonymous user with an API key

Requests made with an API key and no user token can pass the channel's anonymousUserId as userId in products and category.products, so orderlists assigned to the anonymous user are applied. Before, products rejected this with "Unauthorized use of userId" and category.products returned an empty page. When the products of a category cannot be loaded, the response now reports an error for that category instead of an empty page.

Updating or deleting a role that does not exist returns an error

roleDefinitionUpdate created a new role definition at the id you passed when nothing had that id, so a stale or mistyped id produced a phantom record rather than an error. roleDefinitionUpdate, roleUpdate, roleDefinitionDelete and roleDelete now return a not-found error when the target does not exist.

Order status set updates return the statuses in the set

orderStatusSetUpdate returned orderStatuses as null whenever the input left orderStatusIds out, so an update that changed only the name or the priority reported the set as holding no statuses. An update now returns the statuses in the set whichever fields you change. This matters most for clients with a normalizing cache, where that null was written over the cached set and then read back by unrelated queries.

Filter order search by the date a quote stays valid

Order search takes a validUntil filter in both APIs, so you can select quotes by the date they stay valid until. It works like the other date filters and accepts greaterThan and lessThan. Use it to pick up quotes that are about to expire or have already lapsed, instead of reading every order and filtering in your own code.

Order item discount is always an absolute amount

The discount field on an order item is an absolute amount per unit in the order's currency and never a percentage. Both APIs now state this on the field itself, along with how it is calculated (originalPrice minus price, including any item incentive) and how to derive the percentage from it. The order level total.discountValue behaves differently: it is absolute or a percentage depending on total.discountType.

Large carts price and process without hitting a size limit

Processing a cart could fail with "Failed to fetch pricing" once it held enough lines, which put a practical ceiling on how large an order could be. That ceiling is gone. When a line cannot be priced, the error now points at that line rather than failing the whole order.

Clusters and their products stay visible in listings and search

Moving products into a cluster and then deleting the product the cluster started with could hide the cluster and its products from every listing and every search, even though the catalog still held them. Deleting a cluster could leave its products behind in search results instead, which made listing pages arrive short or empty. Listings and search now match the catalog after either change.

Concurrent writes to one cart are rejected instead of dropping lines

Several cartAddItem calls fired at once against the same cart used to answer success for every call while most of the lines were quietly missing from the resulting cart. A cart now takes one item write at a time and rejects a second concurrent write with CART_CONCURRENT_WRITE_ERROR, which also covers cartUpdateItem, cartDeleteItem and cartItemBulk. To add many lines at once, send a single cartItemBulk rather than a burst of separate calls.

Order status sets carry a priority

OrderStatusSet now has a priority field, following the same convention as the priority on an individual order status. Use it to order the sets directly wherever you group orders, quotes or requests by status, instead of deriving an order from the statuses each set contains.

cartAddItem reports a product it cannot add

Adding a product the catalog will not sell to the current Contact, Company or Channel returned a cart with the product quietly missing and no field to check, so a storefront could not tell a successful add from a dropped line. Such a product is now returned under unOrderableItems on the resulting cart instead of under items, so you can detect it and tell the customer why. A line that reaches neither list now fails with CART_ITEM_ADD_ERROR rather than returning a cart with the product silently absent.

Contacts can work on a colleague's cart within the same company

A contact can now read and change a cart that belongs to another contact in the same Company. Until now a cart was reachable only by the contact who created it, so there was no way to build a shared basket, hand a cart to a colleague for approval or let a buyer finish an order someone else started.

Access is bounded by the Company: contacts see each other's carts only within the Company they are acting for.