Skip to main content

Changelog

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

Component
Type

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.

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.

Quote lines report the discount percentage that was applied

A quote line could report a discount of 0% even though the discount had been applied correctly and the line price already reflected it. The percentage on the line now matches the discount that was applied, so a quote you render or export shows the same figure the customer pays.

Package unit quantity accepts the value products are created with

Order handling expected packageUnitQuantity to be a whole number, while creating a product stores it as text, so an order carrying a product with that field could fail. Order handling now accepts the value in the form products are created with.

Attributes on orders and order items

Orders and order lines can now carry attributes, the same way products, companies and contacts do. Define an attribute description once, then set values per order or per line to hold whatever your process needs: a cost center, a delivery instruction, a project code or a reference your ERP expects back.

Values are set while the order is still a cart or a quote and carry through to the order. cartAddItem, cartUpdateItem and the bulk cart item operation all accept attributes on a line, tender items accept them too, and both Order and OrderItem expose an attributes field when you read the order back.

Orders keep the action code they were placed with

An action code applied to an order was not stored on the order or returned by the REST order endpoints, so an integration reading orders could not tell which promotion had been used. The action code is now persisted and exposed on the order.

cartUpdateItem reports why an update failed

cartUpdateItem returned the same generic CART_ITEM_UPDATE_ERROR whatever went wrong, so a client could not tell a cart that no longer exists from an unknown item or an attribute that is not defined on the tenant. The mutation now returns the specific error for each cause, matching what cart and cartAddItem already reported, so you can decide whether to re-resolve the cart, correct the input or surface a configuration problem without an extra query.

Carts through the REST API

Carts are now available over REST as well as GraphQL, at /v2/carts for creating a cart, /v2/carts/{id} for reading, updating and deleting one, and /v2/carts/search for finding carts. This suits an integration that builds or inspects carts from a backend system, where the rest of the work already runs over REST.

Frontends should keep using the GraphQL cart operations, which stay the recommended path for storefronts.

VAT breakdown matches the order total when an order has a bonus item

An order containing a free bonus item reported more VAT in its breakdown than the order total accounted for, so the two did not reconcile. Free bonus items are now excluded from the VAT breakdown, and the breakdown adds up to the order total again.

Clusters reach the search index when the anonymous user has an orderlist

If a channel's anonymous user had a positive orderlist assigned, cluster indexing stopped for the whole tenant and no error was raised. New and updated Clusters never reached the search index, so cluster search returned nothing and catalog listings showed counts without rows, while product indexing carried on normally. Cluster indexing no longer applies the anonymous user's orderlist, so restricting the anonymous catalog with a positive orderlist is safe again.

Create an API key that can be read only once

An API key can now be marked at creation as retrievable only once. The full key value is returned in the creation response and cannot be read again through GraphQL or REST afterwards. Store the value when you create the key, because there is no way to recover it later.

skipMoveOnUpdate applies to the whole batch in large product imports

In a large bulk product import, the skipMoveOnUpdate directive could be applied to only part of the batch, so products that had been organized into other categories were moved back to the category in the payload. The directive now holds across the whole batch regardless of its size.

Products keep their own cluster when several are added to a quote at once

Adding several products to a quote in a single call could return the wrong Cluster against a product, because the cluster rows were paired to products by position rather than by identifier. Products added together from different Clusters now each keep their own, so grouping and cluster options on the quote are correct.

Very large orderlists return their members reliably

Reading an orderlist that had tens of thousands of users assigned to it could fail with a service error rather than returning a page of results. Orderlists of that size now page through their members as expected.

Bulk price imports continue when one source cannot be resolved

A bulk price import failed in full when a single record referenced a source that could not be resolved, so valid records in the same batch were not written either. Source resolution now tolerates a record it cannot match, and the rest of the batch imports.

Search orders by company name

The orders query can now search on company name. Pass COMPANY_NAME in termFields alongside a term to match orders on part of a name, so "Consul" finds orders for "Consultancy Group". The value comes from the linked Company and follows renames, unlike the company names captured on the invoice and delivery addresses, which stay as they were when the order was placed.

Order line descriptions follow the customer's language

Product descriptions copied onto an order could be taken in the wrong language, so a document such as an invoice showed a description in a language the customer does not read even though their language was set correctly. Descriptions now follow the language on the account the order is placed for.

Scope a business rule to specific channels

A business rule can now be limited to one or more channels, so a shipping cost, carrier, payment method, incentive or transaction cost rule applies only where you intend it to. The scope covers the whole rule including its subrules, and it is matched against the channel of the cart or tender being evaluated. Rules without a channel scope apply to every channel in the tenant, so existing rules keep working unchanged.

REST error responses no longer return moreInfo

Error responses from the company, contact and customer endpoints no longer include the moreInfo field. It carried a generic restatement of the error category rather than anything specific to the request. Read type for the error category and messages for the details of what went wrong.

CSV import accepts valid pricesheet discount files

Importing pricesheet discounts from a CSV file returned an error for files that matched the documented format, and the message did not indicate what to change. Files in the documented format now import.

Company names keep ampersands as entered

A company name containing an ampersand was stored with the HTML entity &amp; in place of the character, so a name such as H&A was saved and returned as H&amp;A. Ampersands are now stored and returned as entered.

Skipped records report consistently in bulk company import

When a record in a bulk company import was skipped because it held no changes, the success and action fields in the per-record response did not report that consistently, so a skipped record could read as a failure. Skipped records now return a consistent result you can rely on when checking the outcome of a batch.

Attributes set through bulk import apply to pricing

Attribute values set on companies, contacts and customers through the bulk endpoints were stored on the account but not passed on to pricing, so a pricesheet assigned by user group did not apply to accounts imported that way. Setting the same attribute in Backoffice did work, which made the difference easy to miss. Bulk attribute changes now reach pricing, so imported accounts get the prices their group entitles them to.

Filter product search by inventory quantity

ProductSearchInput now accepts an inventory filter with totalQuantity, localQuantity and supplierQuantity, so you can limit a catalog listing to products above or below a stock threshold instead of fetching everything and filtering in your application. The filter is applied server-side, so itemsFound and the page count match the filtered result set. Queries that do not send the filter behave as before.

Price filtering and sorting follow customer-specific prices

On catalogs that use price sheets, product search returned minPrice and maxPrice as 0 and did not order results correctly when sorting by price. Both the price filter and price sorting now use the price that applies to the requesting customer, so a price slider and price sort work on a catalog page with customer-specific pricing.

Clusters in a hidden category stay out of product listings

A Cluster assigned to a hidden Category still appeared in the product listing of a parent Category when the Cluster's own Category resolved to a visible one. Products in the same hidden Category were already excluded. Hidden Categories now suppress Clusters in the same way, so a hidden branch of the catalog stays out of listings.

Enum filter facets report which values are selected

When a product search was filtered on an enum attribute, the filter was applied to the results but every value in the returned facet came back with isSelected: false. Facets now mark the applied values as selected, so a filter panel that binds its checkboxes to that field shows the active selection.

Company Orderlists apply when browsing by company alone

When the products query received a companyId without a userId, Orderlists assigned directly to that Company were ignored and the full catalog was returned. Orderlists are now resolved from companyId on its own, so a positive Orderlist restricts the results to its products and a negative one excludes them.

Bulk inventory import applies company-specific stock

The bulk inventory endpoint accepted companyId but ignored it, so a record meant for one company's stock was written to general stock instead and the response still reported success. Company-specific records are now created and updated as sent, and general stock is left untouched.