Skip to main content

Upgrading From 3.22 To 3.23

info

To follow a zero-downtime upgrade strategy to 3.23, we recommend first migrating to the latest 3.22.x release and enabling the Celery worker to process all data migrations asynchronously. Otherwise, you will need to schedule downtime to ensure data migrations complete correctly.

Changes in Webhook Emission for Instance Updates​

A new setting, useLegacyUpdateWebhookEmission, has been introduced in shop settings to control the behavior of update webhook emissions. This setting allows users to toggle between the legacy behavior (sending both update webhooks and metadata-specific webhooks for metadata-only changes) and the new behavior (sending only metadata-specific webhooks instead). The legacy behavior is now deprecated as of version 3.22 and will be removed in future versions. The new flow will become the default behavior.

Description​

  • When enabled - legacy behavior: Both update webhooks (e.g., customerUpdated, productVariantUpdated) and metadata-specific webhooks (e.g., customerMetadataUpdated, productVariantMetadataUpdated) are sent for metadata-only changes.
  • When disabled - new behavior: Only metadata-specific webhooks (e.g., customerMetadataUpdated, productVariantMetadataUpdated) are sent for metadata-only changes, and update webhooks are not triggered.

Benefits of Disabling​

Disabling this setting prevents sending update webhooks when only metadata changes are made to the instance, reducing unnecessary webhook traffic.

Applicable Mutations​

This setting applies to all mutations capable of triggering both <object>Updated and <object>MetadataUpdated webhooks.

Dashboard extensions​

Since 3.22, Dashboard extension manifests are validated on the frontend. Installing apps directly via GraphQL is discouraged, as such apps may not work properly.

For Cloud users: You should not be affected by these changes.

For self-hosted:

  • Ensure you use the latest matching Dashboard versions. Early Dashboard v3.22.x releases are not compatible with later Core 3.22 versions. Dashboard v3.23.x and Core v3.23.x are fully compatible.
  • Fields on type AppExtension and AppExtensionManifest: mount, target, and options have been removed. Use the fields mountName, targetName, and settings instead, which directly replace the old ones.
  • New fields are intended to be used by Dashboard only. Modifying them requires caution and may require frontend adjustments.
  • New fields are no longer strictly validated, which means installing an app extension without Dashboard can cause unexpected Dashboard behavior.

Explicit Delivery Options Calculation​

In previous versions, querying delivery-related fields on Checkout like shippingMethods, availableShippingMethods, deliveryMethod and shippingMethod could implicitly trigger sync webhooks (SHIPPING_LIST_METHODS_FOR_CHECKOUT, CHECKOUT_FILTER_SHIPPING_METHODS). Additionally, checkout mutations would silently invalidate the shipping cache, resulting in additional webhook calls.

Deprecation of ShippingMethod ID in checkoutDeliveryMethodUpdate​

Using a ShippingMethod ID as deliveryMethodId in checkoutDeliveryMethodUpdate is deprecated. Use the CheckoutDelivery ID (returned by deliveryOptionsCalculate) instead.

What's New​

Saleor 3.23 makes delivery options calculation explicit by introducing:

  • New mutation: deliveryOptionsCalculate. Returns a list of available deliveries for the checkout. Calling this mutation explicitly fires SHIPPING_LIST_METHODS_FOR_CHECKOUT and CHECKOUT_FILTER_SHIPPING_METHODS webhooks. The mutation replaces the deprecated fields: checkout.shippingMethods and checkout.availableShippingMethods.

  • New field: Checkout.delivery. It represents the currently assigned delivery method. The field replaces the deprecated checkout.deliveryMethod and checkout.shippingMethod.

  • New checkout problems:

See delivery method problems for full handling guidance.

  • Deprecations introduced in 3.23:
    • checkout.shippingMethods - use deliveryOptionsCalculate mutation
    • checkout.availableShippingMethods - use deliveryOptionsCalculate mutation
    • checkout.deliveryMethod - use checkout.delivery
    • checkout.shippingMethod - use checkout.delivery
warning

Querying the deprecated fields (shippingMethods, availableShippingMethods, deliveryMethod, shippingMethod) still works in 3.23, but these fields will be removed in a future release. Requesting these fields can implicitly trigger the SHIPPING_LIST_METHODS_FOR_CHECKOUT, CHECKOUT_FILTER_SHIPPING_METHODS webhooks.

What Has Changed​

  • In previous releases, fetching available shipping methods was implicitly done by requesting the shipping-related fields:
query GetCheckout($id: ID) {
checkout(id: $id) {
availableShippingMethods{
id
}
shippingMethods {
id
name
active
price {
amount
currency
}
}
}
}

Starting from 3.23, there is a dedicated mutation deliveryOptionsCalculate, to fetch deliveries:

mutation DeliveryOptionsCalculate($id: ID!) {
deliveryOptionsCalculate(id: $id) {
deliveries {
id
shippingMethod {
name
active
price {
amount
}
}
}
errors {
field
message
code
}
}
}

Validating the Delivery Assigned to the Checkout​

In previous versions, Saleor was making the validation after each change on Checkout, causing multiple webhooks called implicitly. This slows down the checkout process, by increasing the network traffic and costs. Starting from 3.23, Checkout.problems can contain the problem: CheckoutProblemDeliveryMethodStale.

query Checkout($id: ID!) {
checkout(id: $id) {
problems {
... on CheckoutProblemDeliveryMethodStale {
__typename
}
}
}
}

The problem provides the information that mutation call: deliveryOptionsCalculate is needed to confirm that stale delivery method is still valid for the checkout. The storefront decides when to call deliveryOptionsCalculate.

info

Calling checkoutComplete with existing CheckoutProblemDeliveryMethodStale will implicitly refresh the delivery method.

Handling Invalid Delivery Assigned to the Checkout​

In previous versions, Saleor was implicitly removing the delivery method that was not valid anymore. In such case, the delivery-related fields were returning null. Starting from 3.23, the invalid delivery is represented by the new problem. The checkout.delivery field returns the invalid delivery assigned by customer:

query Checkout($id: ID!) {
checkout(id: $id) {
delivery {
id
}
problems {
... on CheckoutProblemDeliveryMethodInvalid {
__typename
}
}
}
}

Saleor won't unassign it implicitly anymore. The storefront decides when to change the invalid delivery method.

info

Calling checkoutComplete with existing CheckoutProblemDeliveryMethodInvalid will return error.

Migration steps​

  • Replace fetching shipping methods:
    • Instead of checkout.shippingMethods, checkout.availableShippingMethods, use the explicit mutation: deliveryOptionsCalculate(id: <checkoutID>)
  • Handling new checkout problems
    • When CheckoutProblemDeliveryMethodStale appears in checkout.problems:
      • The assigned shipping method is considered stale.
      • To verify its applicability, call deliveryOptionsCalculate. Based on the response:
        • If CheckoutProblemDeliveryMethodStale is removed from checkout.problems, the shipping method is still applicable.
        • If CheckoutProblemDeliveryMethodStale is replaced with CheckoutProblemDeliveryMethodInvalid, the shipping method is no longer valid.
    • When CheckoutProblemDeliveryMethodInvalid appears in checkout.problems:
      • The assigned shipping method is not applicable.
      • A new shipping method must be selected by calling deliveryOptionsCalculate to retrieve valid options.
      • checkoutDeliveryMethodUpdate needs to be called to replace non-applicable shipping method.
  • Updating shipping method checks
    • Replace usage of checkout.deliveryMethod or checkout.shippingMethod with checkout.delivery
    • Replace conditions like checkout.deliveryMethod == null or checkout.shippingMethod == null with:
      • Shipping is not applicable anymore: checkout.problems contains CheckoutProblemDeliveryMethodInvalid.
      • Shipping has not been set: checkout.delivery == null.

Removal of Legacy Digital Products Flow​

If you were using the old digital content API:

  1. Remove all usage of:

    • Queries: digitalContent, digitalContents
    • Mutations:
      • digitalContentCreate
      • digitalContentUpdate
      • digitalContentDelete
      • digitalContentUrlCreate
  2. Update deprecated product type filtering and sorting:

    The following fields are deprecated and will be removed in v3.24.0:

    • productTypes(filter: { productType: DIGITAL }) - instead, use metadata or attributes to represent and filter digital product types.
    • productTypes(sortBy: { field: DIGITAL }) - instead, use sortBy: { field: SHIPPING_REQUIRED }.
  3. Migrate to the official supported solution by following the official guide

    In short:

    • Store digital files externally (e.g., AWS S3 or similar)
    • Use metadata to associate files with variants/orders
    • Use webhooks (e.g., order fulfillment events) to:
      • Generate download links
      • Send them to customers

Removal of Deprecated EditorJS Behaviors​

  • EDITOR_JS_LINK_REL: the default behavior has changed. Links rendered by EditorJS (<a href="..." rel="...">) now include rel="noopener noreferrer" by default (previously, no rel attribute was added).

    Either:

    1. (Recommended) Do nothing and accept the new default (noopener noreferrer)
    2. Or, set the environment variable EDITOR_JS_LINK_REL to an empty value (e.g., export EDITOR_JS_LINK_REL=) to retain the previous behavior
  • UNSAFE_EDITOR_JS_ALLOWED_URL_SCHEMES: this setting has been removed. It is no longer possible to extend the list of allowed URL schemes via configuration.

    This change may affect content relying on custom or non-standard URL schemes. If you require support for additional URL schemes, open a request: https://github.com/saleor/saleor/issues

EditorJS Parser Strictness​

The EditorJS parser no longer accepts unknown or extra fields in content blocks. Previously, unexpected fields were accepted. If you only use Saleor Dashboard to edit rich-text descriptions, then nothing needs to be done. Only the following EditorJS extensions are supported:

Any other extension is unsupported and will lead to parsing errors when running mutations such as productUpdate(). If you need support for additional extensions, open a request: https://github.com/saleor/saleor/issues

Asynchronous Image Fetching for Product Media​

Previously, productMediaCreate and productBulkCreate mutations blocked until external images (provided via mediaUrl) were fully downloaded and processed. Starting with version 3.23, images from external URLs are fetched asynchronously in the background.

What Has Changed​

  • When creating product media with an external image URL, the mutation now returns immediately without waiting for the image to be downloaded.
  • The image is fetched in the background by a Celery task with automatic retries (up to 5 attempts with exponential backoff).
  • While the image is being fetched:
    • The url field on ProductMedia returns a proxy URL that responds with HTTP 503 ("Image has not been fetched yet, try later.") until the image is downloaded and stored.
    • Thumbnail requests for pending images also return HTTP 503 with the same message.
  • If the image fetch fails permanently (after all retries), the ProductMedia entry is deleted.

Required client changes​

  1. Handle HTTP 503 responses when requesting thumbnails or media URLs. This status code indicates the image is still being processed. Implement a retry mechanism with appropriate backoff.

  2. Do not assume images are available immediately after productMediaCreate or productBulkCreate returns. The media object will exist, but the image may not be ready yet.

  3. ProductMedia.url no longer returns null for missing images: Previously, when a ProductMedia had no stored image, the url field returned null. Now it returns a proxy URL instead. If your client code checks for null on ProductMedia.url to detect missing images, you need to update that logic — the URL will always be returned, but it may respond with HTTP 503 if the image is still being fetched.

Affected mutations​

info

This change only affects media created via external image URLs (mediaUrl). Direct file uploads and video/oEmbed URLs are not affected.

New /image/ endpoint​

A new URL endpoint has been added: /image/<instance_id>/. While the image is being fetched, this endpoint returns HTTP 503. Once the image is stored, it redirects to the image file.

If your CDN or reverse proxy is configured to allowlist specific Saleor URL patterns (e.g., /thumbnail/, /media/), you will need to add /image/ to the allowed paths.

Password Login Mode​

A new passwordLoginMode setting has been added to Shop / SiteSettings, allowing administrators to control password-based authentication. This is especially useful for organizations transitioning staff to OIDC/SSO.

Three modes are available:

  • ENABLED (default) — password login works normally for all users. This is backward-compatible; existing deployments are unaffected.
  • CUSTOMERS_ONLY — customers can still log in with passwords, but staff tokens are issued with is_staff=false and no permissions.
  • DISABLED — all password-based mutations (tokenCreate, passwordChange, setPassword, requestPasswordReset) are blocked for all users.

For full details, including configuration examples and migration steps, see the Password Login Mode documentation.

Removal of Adyen Plugin​

The legacy Adyen payment gateway plugin has been removed from Saleor core. If you were using the Adyen plugin, switch to the Adyen App before upgrading. Remove any plugin configuration referencing mirumee.payments.adyen from your environment.

Removal of NP Atobarai Plugin​

The legacy NP Atobarai payment gateway plugin has been removed from Saleor core. If you were using this plugin, switch to the NP Atobarai App. See the migration from plugin guide for detailed steps. Remove any plugin configuration referencing the NP Atobarai plugin ID from your environment.

Removal of Payment.partial Field​

The partial field has been removed from the Payment GraphQL type. It was only used by the removed Adyen integration, so likely you haven't used it if you haven't used the Adyen plugin. If your queries or client code references Payment.partial, remove it from your GraphQL selections. This field is no longer available in the schema.

staffDelete Behavior Change​

The staffDelete mutation now fully deletes staff users from the database, regardless of whether they have associated order history. Previously, staff users with orders were only deactivated instead of deleted. Review any workflows that rely on deactivated staff accounts remaining in the system. If you need to preserve staff user data, export it before deletion.

Removal of JWT_EXPIRE Setting​

The JWT_EXPIRE configuration option, which allowed ignoring JWT token expiration, has been removed. If you had JWT_EXPIRE set in your environment, remove it. All JWT tokens are now subject to standard expiration rules. Ensure your clients handle token refresh correctly.

Attribute Fields Are Now Non-Nullable​

The fields name, slug, and type on the Attribute GraphQL type are now non-nullable — they always return a value. If your client code has null-checks or optional chaining for Attribute.name, Attribute.slug, or Attribute.type, those checks are no longer necessary. Update your types or codegen accordingly.

RefundSettingsUpdate Mutation Change​

The refundSettings field in the RefundSettingsUpdate mutation response is now nullable, correctly reflecting null when errors occur. If your client code assumes refundSettings is always present in the response, add a null-check to handle error cases.

Shipping Method Metadata Denormalization​

Shipping method metadata is now copied to dedicated order fields during checkout-to-order conversion. After an order is created, updates to the original shipping method's metadata are no longer reflected in the order's shippingMethod.metadata. If your integration reads shipping method metadata from orders and expects it to stay in sync with the shipping method object, update your logic. To update metadata on existing orders, modify the order's shipping method metadata directly.

GraphQL Federation Type Change​

The representations field type in the _entities query has changed from [_Any] to [_Any!]! (non-nullable list of non-nullable items). If you use Apollo Federation or a custom gateway that interacts with Saleor's _entities query, ensure your gateway is compatible with the stricter type. Update any federation tooling that may break on the non-nullable constraint.

New NonNegativeInt Scalar and Stricter Time Unit Scalars​

A new NonNegativeInt GraphQL scalar type has been added that rejects negative integers at the schema level. The existing Minute, Hour, and Day scalars now inherit from NonNegativeInt instead of Int, enforcing non-negative values directly in the GraphQL layer.

This is a breaking change: channelCreate and channelUpdate mutations now raise GraphQL type errors when negative time values are provided (e.g., negative orderMarkAsPaidStrategyTimeout). Previously, they returned an INVALID business error code.

Migration steps​

  • If you have error handling that checks for INVALID codes from channel mutations when passing time values, update it to handle GraphQL-level type errors instead.
  • Update your codegen: If you use a GraphQL code generator (e.g., graphql-codegen), add the new NonNegativeInt scalar to your scalar type mappings. For example, in your codegen config:
    scalars:
    NonNegativeInt: number
    Re-run your code generator to pick up the new scalar and the updated Minute, Hour, and Day types.

AppInstallInput Now Requires appName and manifestUrl​

The AppInstallInput type now requires both appName and manifestUrl fields in the schema. If you call app installation mutations, ensure you always provide both appName and manifestUrl. Requests missing either field will now fail with a schema validation error.

Gift Card Payment Support via Transaction API​

Gift cards can now be used as a payment method through the Transaction API using a built-in payment gateway (saleor.io.gift-card-payment-gateway). This allows applying gift card balances during checkout via transactionInitialize.

If your storefront supports gift cards, you can integrate them as a native payment option without a third-party payment app. See the Gift Cards as payment method guide for setup details including mutation examples and gateway configuration.

OIDC Password Invalidation​

When an existing user account is claimed by an OIDC provider, the user's password is now automatically invalidated. If your users authenticate via both password and OIDC, be aware that once an OIDC provider claims their account, password login will no longer work. Users must use the OIDC provider going forward, or reset their password if password login is still enabled.

Deprecations​

Deprecated fields will be removed in next versions. To make future upgrades easier, we recommend to remove deprecated fields as soon as possible.

Deprecation of hasVariants on ProductType​

The hasVariants field on ProductType is deprecated. It was a legacy artifact that is no longer necessary. Stop relying on hasVariants in your queries. Instead, check whether a product has variants by querying the variants field directly.

Deprecation of Export Mutations​

The mutations exportProducts, exportGiftCards, and exportVoucherCodes are now deprecated.

To export these entities, use standard GraphQL queries.

Deprecation of voucher Input on Draft Orders​

The voucher field on DraftOrderInput and DraftOrderCreateInput is deprecated. Use the voucherCode field instead when creating or updating draft orders.