Skip to main content

Release notes

Release 1.50.1

2026-09-22

Fixed

  • shippingOptions, cartShippingOptions — a single shipping method that could not be assembled no longer fails the whole query.

  • shippingOptions, shippingOption, shippingOptionSelections, cartShippingOptions — a failure while recording internal usage statistics no longer fails the query.

Release 1.49.1

2026-09-22

New

  • propertyConnection — a new paginated query for product property definitions.

  • Availability.overrideVisible — tells you whether an availability status allows a product to be shown on its own product page even though it is hidden in listings.

  • ProductFilter.overrideVisible — set to true on productConnection to include products that are only partially visible, i.e. hidden from listings but reachable on their own product page. Default behaviour is unchanged.

Changed

  • product(id:) now returns products that are only visible on their own product page. Previously such a product resolved to nothing even when you had its id. Variant groups follow the same rule, so a variant that is only visible on its product page now resolves its group and siblings correctly.

  • Shipping options are no longer filtered on the integration-type flag of the shipping method. All active methods on the cart's shipping class are now returned. If you have methods configured that never appeared in shippingOptions, expect them to show up now.

Fixed

  • Amounts sent to Kustom and Svea were truncated rather than rounded when converted to minor units. A price like 16.99 could be sent as 1698 instead of 1699, which meant a payment total or a single order line could end up an öre below the corresponding cart total. Order lines, fees, totals, discount amounts and tax rates are all affected. Totals reported by the API itself were always correct — this was the amount handed to the payment provider.

  • The Svea invoice fee was returned with its inclusive price in minor units rather than in the currency, so a 29 kr fee appeared as 2900. Its tax value was also left at zero and the tax percent was unrounded. The fee now reports a correct inclusive price, excluding price, tax value and tax percent, using the currency's standard VAT rate.

Documentation

  • CREATED_AT and UPDATED_AT in the product dynamic filter document that they accept a full datetime (2006-01-02T15:04:05Z07:00) as well as a plain date. Values given without a timezone are interpreted as UTC. This is a documentation correction; the behaviour was already there.

  • productConnection(after:) — the description said the cursor was a customer ID. It is the product ID of the last item on the previous page.

Release 1.48.1

2026-09-17

Changed

  • BatchProductPropertyInput.value now accepts rich HTML. The field type changed from String! to RichHTML!, so property values sent to batchCreateProducts and batchUpdateProducts keep safe formatting and media markup (<strong>, <em>, <p>, <br>, lists, links, images and similar) instead of having every tag stripped and <, > and & escaped into entities. Unsafe markup — <script>, <style>, event handler attributes and the like — is still removed.

    Breaking, for one case: if your operation declares the property value through a variable typed as String! — for example mutation ($value: String!) passed to value: $value — the operation no longer validates and will be rejected. Change the variable declaration to RichHTML!. Operations that pass the value inline, or that already use an untyped/matching declaration, are unaffected, and values that contain no HTML behave exactly as before apart from the whitespace note above.

Release 1.47.1

2026-09-17

Changed

  • ProductDynamicFilterField.ACTIVE (breaking) — the field now takes a boolean string: true, TRUE or 1 for active products, false or 0 for cancelled ones. Previously it only accepted the two raw storage values, where the value for active was the counter-intuitive "N", so a filter that read as "active" in the query selected the opposite set. Those raw values are now rejected: the condition is dropped, the rest of the group still applies, and the response carries an error alongside the unfiltered result. If you filter on ACTIVE today, switch to true/false — otherwise your filter silently stops narrowing the result set.

Release 1.46.1

2026-09-16

New

  • Product.accessories(overrideVisible: Boolean) — new optional argument on the accessories field. When true, an accessory list also returns products whose stock status hides them from normal listings but still marks them as viewable on their own product page. The argument defaults to false, so existing queries are unaffected.

Release 1.45.1

2026-09-15

New

  • ChosenBundlePrice.unitPrice — the price of one bundle at the requested quantity, alongside the existing totalPrice. totalPrice is unitPrice times the requested quantity, so quantity-dependent (tiered) bundle pricing is now visible per unit instead of only in the total.

Changed

  • salesStatistics is now Stable — the query and its SalesStatisticsDimension, SalesStatisticsMetric, SalesStatisticsRow, filter and input types are promoted from Upcoming to Stable. Behaviour is unchanged; the shape is now settled and will not change without notice. It still requires an admin user and an API key with the STATISTICS_READ permission.

Fixed

  • Bundle rows in the cart were priced by the quantity twice. CartRow.unitPrice returned the price of the whole row rather than of one bundle, and CartRow.price then multiplied that by the quantity again, so a bundle row with quantity 2 showed four times the bundle price, and quantity 3 nine times — inflating the cart total with it. Rows with quantity 1 were correct. unitPrice is now the price of one bundle and price is the row total.

  • Bundle rows never showed a before-discount price. The regular price on a bundle cart row was copied from the sale price, so a discounted bundle looked undiscounted. regularPrice on both unitPrice and price now carries the real before-discount amount.

  • A bundle cart row without its bundle data no longer fails the whole request. It now reports a warning for that row instead of aborting the query.

  • Customer.addresses no longer fails for customers with a missing address.

Release 1.44.1

2026-09-14

Changed

  • Postal code restrictions on shipping methods now match by prefix. A shipping method limited to 662 now covers 66234; previously the customer's whole postal code had to appear in the configured list, so prefix rules matched nothing and the method was offered or hidden for the wrong addresses. This affects shippingOptions, shippingOption and cartShippingOptions. If your store has postal code rules written as prefixes, the set of options a customer sees will change..

  • Currency.prefix and Currency.suffix are null when not configured. They previously came back as empty strings. If you test for an empty string, test for null as well.

  • Shipping options are resolved in batched queries. Validation and interval pricing for all methods in a request now run as one query each instead of one per method. Results are unchanged; responses are faster on stores with many shipping methods.

Fixed

  • Shipping prices carried an incomplete currency. The currency on a shipping option's price — and on a free-shipping price — was returned with only its code and rate, dropping prefix and suffix. All shipping prices now carry the full currency.

  • A shipping method with a price interval but no price failed the whole request. When the interval matching the cart value had no price configured, shippingOptions returned an error instead of any options. That method now falls back to its normal configured price.

  • Bundles containing an always-buyable product were removed from the cart. When a bundle's chosen sub-product is buyable whether or not it is in stock, the reservation check still measured it against its stock balance, so the bundle row was reported as out of stock and removed once the reservation expired. If the bundle was the only row, the cart then held no rows and cart came back null — an emptied cart from the storefront's point of view. Such rows now survive both reconcileCartReservations and an ordinary cart read.

  • Always-buyable products could not be added beyond their stock balance. addToCart rejected them with an out-of-stock error even though the product is sold when out of stock. Products that require a reservation are still limited by what is actually available.

  • A bundle category configured with a quantity of zero broke the reservation check. reconcileCartReservations failed for any cart holding such a bundle. It now completes and leaves the row alone.

  • Reservations could expire the moment they were extended. The new expiry was written from the API's clock and compared against the database's, so where the two ran in different time zones a reservation was stored as already expired — every cart read then reconciled the cart and adjusted it down. Expiry is now both written and read on the database clock, and the returned expiry is the value actually stored.

  • A removed bundle row now reports the sub-product that caused it. When several sub-products failed, details.subProductId could name a later one instead of the one that triggered the removal.

  • Kustom Checkout no longer loses its fallback shipping when the options cannot be determined.

Release 1.43.2

2026-09-09

No release notes.

Release 1.43.1

2026-09-09

New

  • productConnection(dynamicFilter: ProductDynamicFilter) — build your own product filters instead of picking from the fixed ones. A filter holds groups, each group holds conditions of field, operation and values, and both levels combine with AND or OR (AND when omitted), giving two levels of nesting. ProductDynamicFilterField covers id, name, description, EAN, price, campaign price, stock, weight, brand, supplier, owner, category, default category, VAT, shipping class, availability, product type, active state, creation and update dates, and the language, price list and domain restrictions. ProductDynamicFilterOperation offers EQUALS, CONTAINS, IN, LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL, EMPTY and NOT_EMPTY. Requires an admin user. It cannot be combined with filter — passing both returns an error and no products.

    Values are always given as strings and converted per field; each enum value documents what it expects (an integer id, a float, a date such as 2006-01-02, or plain text). A condition with the wrong number of values for its operation, or a value that cannot be converted, is dropped: the query still returns the products matching the remaining conditions and reports the dropped ones as an error entry alongside the result, so check errors even on a successful response.

  • ProductFilter.categoryIds and ProductFilter.brandIds — narrow productConnection to products in the given categories or of the given brands, without reaching for the dynamic filter.

  • Product.manufacturer and Product.importer — the responsible economic operators behind a product, for EU General Product Safety Regulation notices. ProductManufacturer and ProductImporter both carry id, name, contactPerson, website, note and an address of the new type CONTACT, plus internalName, which requires an admin user.

  • OAuth 2.1 authorization server for administrator accounts. Administrators can now sign in to a third-party client through a browser redirect instead of the client handling their password. The server exposes /oauth/authorize, /oauth/token, /oauth/register and the discovery documents /.well-known/oauth-authorization-server and /.well-known/jwks.json. It supports the authorization_code and refresh_token grants, requires PKCE with S256, registers public clients dynamically with no client secret, and prompts for the second factor when the administrator has one configured. The access token you receive is the same token the GraphQL admin login issues and is accepted everywhere that one is, so no other part of your integration changes.

Changed

  • AddressType has a new value, CONTACT. It is used for manufacturer and importer addresses. Existing billing and shipping addresses are unaffected, but if your client rejects or crashes on unknown enum values, handle it before reading these fields.

Fixed

  • updateCustomer silently discarded most billing address fields. Only street address, postal code and city were saved on billingAddress; firstName, lastName, streetAddress2, streetAddress3, houseNumber, houseExtension, phone, email, countryCode and stateCode were accepted and then dropped. On standardAddress, houseExtension and stateCode were dropped the same way. The mutation reported success throughout, so the values simply never appeared on the next read. All of these are now stored.

  • Campaign prices depended on how products were paginated. When several products were fetched in one request, only one campaign offer per campaign survived, so a product could be given another product's campaign price or lose its campaign entirely — and the same product could come back with a different price on a different page or in a differently sized batch. Each product now gets its own offer.

  • A product with several campaigns of the same type got an arbitrary one. The tie-break between equally typed campaigns was unstable, so identical requests could return different campaign prices. The configured priority, and then the best price, now decide, and repeated requests agree.

  • LowestPrice.productIds listed variants that were not at the lowest price.

Release 1.42.1

2026-08-28

Breaking

  • order(id:, checkoutId:) — the checkoutId argument is gone. It previously let a caller who was not logged in fetch an order by passing the matching checkout id, which is how order confirmation pages usually worked. order now always requires a logged-in user, and a customer token reaches only that customer's own orders. For a confirmation page, read the order through checkout(checkoutId:, secretKey:) { order { … } }, or take it from finalizeCheckout when you pass the secret key.

  • The settings query has been removed, along with the Settings type and the SettingsFilter input. themeSettings is unaffected

  • createOrder now requires an input with at least one row. input changed from CreateOrderInput to CreateOrderInput!, and rows from [OrderRowInput!] to [OrderRowInput!]! with a minimum of one. Calls that omitted either are now rejected during validation instead of failing later.

  • Text you send is no longer HTML-escaped. Every String input is still stripped of HTML tags, but the characters that remain — &, <, >, ", ' — are now stored and returned as written. A customer named O'Brien used to come back as O&#39;Brien; it now comes back as O'Brien.

New

  • salesStatistics — aggregated sales figures in one query, grouped by up to three SalesStatisticsDimension values (day, week, month, year, country, brand, category, product, customer) over up to ten SalesStatisticsMetric values (quantity, revenue excluding and including tax, tax amount, order count, customer count, new customer count). Takes a required dateRange, an optional filter and orderBy, and caps rows at limit (default 1000, max 5000) with a truncated flag when it bites. Requires an admin user and an API key with the new STATISTICS_READ permission. Marked Upcoming — give feedback before it settles.

  • orderWithdrawalConnection and Order.withdrawals — right-of-withdrawal cases with their status (NEW, HANDLED, REJECTED), customer comment, timestamps and the order rows they cover, filterable by withdrawal, order, customer, status and creation date. Needs an API key with ORDER_READ and, for an admin user, API_ORDER_MANAGE. A customer token sees only withdrawals on that customer's own orders. The log field is admin-only.

  • Signed shipping selections. Every ShippingOption, selection and addon now carries a secureId — an encrypted token holding the option's id and price. Send the customer's choice back as CheckoutInput.selectedShipping (a SecuredSelectedShippingInput with the option's secureId plus any chosen addons and selection), and the API validates it and stores the result as Checkout.selectedShipping and Order.selectedShipping: a SelectedShipping with totalPrice and a breakdown of option, addons and selection. validateShippingSelection resolves a selection without storing it. Tokens expire — two hours by default, configurable per store — so re-fetch options if the customer sits on the checkout page. The old shippingMethodId and pickupPointId fields still work but are deprecated.

  • updatePage and updateHomePage — update a page's or home page's content, active flag, and for home pages a publication timeslot, per language. Requires an admin user and an API key with the BETA permission.

  • giftCardConnection(sort:) — sort gift cards by ID, CREATED_AT or EXPIRES_AT in either direction.

  • OrderRowInput.unitPrice — the price per single unit of an order row. The existing totalPrice was always treated as a unit price despite its name; it is now deprecated and unitPrice wins when both are given.

  • RowCampaignType.BUY_X_PAY_Y — order and cart rows discounted by a buy-X-pay-Y campaign now report an appliedCampaign with this type instead of none.

  • X-User-Token — send the user's access token in this header. Authorization still works and now also accepts a Bearer prefix, which was previously rejected as an invalid token.

Changed

  • An invalid user token now answers 401 Unauthorized instead of 403 Forbidden. An invalid API key still answers 403.

  • Shipping options and pickup points are no longer cached. Every request now goes to the carrier, so results are always current; expect responses to take longer where a cache used to answer. Carrier timeouts were raised from two to three seconds at the same time, so slow carriers now make it into the result more often instead of dropping out.

  • availableCampaigns.upsellCampaigns now applies context rules when you filter by ids. Passing ids used to bypass the check, returning campaigns that were not valid for the customer's domain, price list or country.

  • Deprecations, all still functional: Checkout.shippingMethodId, Checkout.pickupPointId and the same two fields on CheckoutInput (use selectedShipping); Order.shippingDetails.pickupLocation and .shippingMethodId (use Order.selectedShipping); OrderRowInput.totalPrice (use unitPrice); the shippingMethods and pickupPoints queries and ShippingMethod.pickupPoints (use cartShippingOptions, shippingOptions and a shipping option's selections).

Fixed

  • createOrder failed when a row had no price. The input documented leaving the price empty to use contextual pricing, but doing so made the mutation fail. Rows without a price now use the product's current price for the request's currency and customer context, preferring the sale price.

  • addToCart and removeFromCart reported a misleading error. When the cart did not exist and the inventory check failed, the response also complained that the cart did not exist, burying the real reason. The cart field is now simply null and the inventory result carries the explanation.

  • A payment provider hiccup could create a duplicate payment order. When updating an existing Svea checkout order came back as anything but success, the reference was discarded and a fresh order created, even if the original was alive. The existing order is now checked first and only replaced when it is genuinely cancelled or gone. A network failure to the provider also crashed the request instead of returning an error.

  • A postal code with no nearby DHL service points returned an error for the whole request. It now returns an empty list of selections.

  • An unrecognised country name on a customer or address made the query fail. Country lookup now also matches translated country names, and an unknown name resolves to no country instead of an error.

  • giftCardConnection paged incorrectly. Cursor paging with after assumed ascending id order, so it skipped or repeated rows under any other ordering. Paging now follows the requested sort.

  • Localized shop texts could come back in the wrong language. Texts were cached without the language, so whichever language asked first was served to everyone until the entry expired. This affected shipping addon titles, fee row names on orders and the contact form subject.

  • A discount code linked to a parent code ignored the parent's terms. Order-level discount amount, percentage and free shipping are now inherited from the parent code, so CouponCode reports what the code actually does.

  • Contact form submissions arrived with escaped characters and no line breaks. The mail is now sent as HTML with the submitted values in readable form.

  • New orders record their stated delivery time. Order.shippingDetails.scheduledDeliveryAt is now filled in when an order is created, in the order's language, instead of being left empty.

Documentation

  • Access restrictions are now stated in the schema for the operations that carry them, so orderConnection, order, returnConnection, customerConnection, the batch mutations, createOrder, createBrand and jobStatus say in their descriptions whether they need an admin user and what a customer token sees. Several operations were also marked Stable: createOrder, createCustomer, updateCustomer, batchCreateCustomers, initiateResetCustomerPassword, submitContactForm, logout and version.