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.
All platform changes in one place. Filter by component or type to find what matters to you.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 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.
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.
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.
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.
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.
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.
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.
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.
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 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 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 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
A company name containing an ampersand was stored with the HTML entity & in place of the character, so a name such as H&A was saved and returned as H&A. Ampersands are now stored and returned as entered.
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.
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.
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.
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.
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.
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.
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.
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.