> Documentation index: [Saleor](/llms.txt) · [This section](/upgrade-guides/llms.txt)
> Source: https://docs.saleor.io/upgrade-guides/core/3-20-to-3-21

# Upgrading From 3.20 To 3.21

info

To follow the zero-downtime strategy when upgrading to 3.21, **It is recommended to first migrate to latest 3.20.X** and turn on the Celery worker to process all data migrations asynchronously. Otherwise, you will need to downtime your solution to ensure correct data migration.

<a id="authentication-required-for-changing-order-and-orderline-metadata"></a>

## Authentication required for changing `Order` and `OrderLine` metadata

The [updateMetadata](/api-reference/miscellaneous/mutations/update-metadata.md) mutation for `Order` and `OrderLine` now requires the `MANAGE_ORDERS` permission. Once an order is created, customers cannot edit its metadata. However, merchants and apps with the necessary permissions can modify this data. Fetching metadata remains unchanged.

<a id="merging-metadata-in-transactionupdate-mutation"></a>

## Merging metadata in `transactionUpdate` mutation

The [transactionUpdate](/api-reference/payments/mutations/transaction-update.md) mutation now merges the provided metadata and private metadata with existing values, instead of overwriting them. To delete a specific metadata entry, use the [deleteMetadata](/api-reference/miscellaneous/mutations/delete-metadata.md) mutation. For private metadata, use the [deletePrivateMetadata](/api-reference/miscellaneous/mutations/delete-private-metadata.md) mutation.

<a id="checkoutshippingmethods-and-checkoutavailableshippingmethods-do-not-return-external-shipping-methods-if-their-currency-differs-from-the-checkouts-currency"></a>

## `checkout.shippingMethods` and `checkout.availableShippingMethods` do not return external shipping methods if their currency differs from the checkout's currency

If an app subscribed to `SHIPPING_LIST_METHODS_FOR_CHECKOUT` returns an external shipping method with a currency different from the checkout's currency, Saleor will filter out that shipping method. As a result, external shipping methods with a different currency than the checkout's currency will not be returned via the API (in the checkout.shippingMethods and checkout.availableShippingMethods fields). Any potential currency validation for external shipping methods on the frontend / app side is no longer necessary.

<a id="blocked-external-calls-for-checkout-and-order-bulk-queries"></a>

## Blocked external calls for checkout and order bulk queries

To enhance performance and efficiency, bulk queries that fetch multiple checkout or order objects will **no longer trigger external calls to third-party services**. The following synchronous webhooks will be affected:

-   **`CHECKOUT_CALCULATE_TAXES`** webhooks
-   **`ORDER_CALCULATE_TAXES`** webhooks
-   **`SHIPPING_LIST_METHODS_FOR_CHECKOUT`** webhooks
-   **`CHECKOUT_FILTER_SHIPPING_METHODS`** webhooks
-   **`ORDER_FILTER_SHIPPING_METHODS`** webhooks

External calls from the **AvataxPlugin** will also be skipped.

Instead of recalculating taxes externally, the system will return **pre-stored prices** from the database. Additionally, only built-in shipping methods will be returned. The `CHECKOUT_FILTER_SHIPPING_METHODS` and `ORDER_FILTER_SHIPPING_METHODS` webhooks will not be triggered, and all built-in shipping methods will be listed as available.

Any external shipping methods assigned to the checkout will still be visible through denormalized data stored in the database.

<a id="affected-queries"></a>

#### Affected queries

The following queries will **not trigger external calls**:

-   `checkouts(...)`
-   `me.checkouts(...)`
-   `checkoutLines(...)`
-   `orders(...)`
-   `me.orders(...)`
-   `draftOrders(...)`

<a id="example-of-impacted-queries"></a>

#### Example of impacted queries

```graphql
{
  checkouts(first: 10) {
    edges {
      node {
        id
        deliveryMethod {
          ... on ShippingMethod {
            id
            name
          }
        }
        shippingMethods {
          id
          name
        }
        totalPrice {
          gross {
            amount
          }
        }
      }
    }
  }
}
```

```graphql
{
  orders(first: 100) {
    edges {
      node {
        deliveryMethod {
          ... on ShippingMethod {
            id
            name
          }
        }
        shippingMethods {
          id
          name
        }
        total {
          gross {
            amount
          }
        }
      }
    }
  }
}
```

note

The `checkout` and `order` queries will still work as before. If tax information is outdated, external calls will be made to recalculate taxes. External shipping methods and filters will be fetched if necessary.

<a id="checkoutprivatemetadata-no-longer-stores-external-shipping-method-id"></a>

## `checkout.privateMetadata` no longer stores `external-shipping-method-id`

The `external-shipping-method-id` is no longer stored within `checkout.privateMetadata`. To retrieve this ID, use the `checkout.deliveryMethod` field instead.

<a id="checkoutdeliverymethodupdate-and-checkoutshippingmethodupdate-mutations-do-not-trigger-checkout_updated-webhook-or-tax-invalidation-when-adding-the-same-method-already-assigned"></a>

## `checkoutDeliveryMethodUpdate` and `checkoutShippingMethodUpdate` mutations do not trigger `CHECKOUT_UPDATED` webhook or tax invalidation when adding the same method already assigned

Previously, the `checkoutDeliveryMethodUpdate` and `checkoutShippingMethodUpdate` mutations would trigger the `CHECKOUT_UPDATED` webhook and invalidate taxes regardless of whether the same delivery method was already assigned to the checkout. Starting from version 3.21, if the same method that is already assigned to the checkout is provided, the `CHECKOUT_UPDATED` webhook will not be triggered, and taxes will not be invalidated. From Saleor's perspective, nothing has changed, as the checkout remains in the same state as it was before the mutation was called.

<a id="introduced-price-freeze-period-for-editable-orders"></a>

## Introduced price freeze period for editable orders

In previous versions, all editable orders (those with `DRAFT` and `UNCONFIRMED` statuses) refreshed their base prices by fetching them from the current product catalog during each recalculation. Additionally, all discounts were applied based on the latest reward values.

To ensure price consistency over time, regardless of updates to the product catalog, Saleor has introduced a price freeze period. During this period, order lines use denormalized base prices instead of retrieving them from the catalog. More details can be found [`here`](/developer/price-freeze-period.md)

-   Draft orders: the base prices are frozen for 24 hours by default. This duration can be modified using the `Channel.draftOrderLinePriceFreezePeriod` setting.
-   Unconfirmed orders: the base prices remain frozen indefinitely, and this setting is not configurable.
-   Other orders: remain unchanged and cannot be recalculated.

<a id="draftorderupdate-mutation-no-longer-calls-draft_order_updated-webhook-if-nothing-has-changed"></a>

## `DraftOrderUpdate` mutation no longer calls `DRAFT_ORDER_UPDATED` webhook if nothing has changed

The `DraftOrderUpdate` mutation no longer triggers the `DRAFT_ORDER_UPDATED` webhook event if there are no changes to the draft order. This adjustment ensures that the event is only fired when there are actual updates to the draft order, reducing unnecessary event emissions.

<a id="orderupdate-mutation-no-longer-calls-order_updated-webhook-if-nothing-has-changed"></a>

## `OrderUpdate` mutation no longer calls `ORDER_UPDATED` webhook if nothing has changed

The `OrderUpdate` mutation no longer triggers the `ORDER_UPDATED` webhook event if there are no changes to the draft order. This adjustment ensures that the event is only fired when there are actual updates to the order, reducing unnecessary event emissions.

<a id="required-transactionid-field-in-ordergrantrefundcreateinput"></a>

## Required `transactionId` field in `OrderGrantRefundCreateInput`

The `OrderGrantRefundCreateInput` now requires a `transactionId` field. This change affects the `OrderGrantRefundCreate` mutation. The `transactionId` must be provided as part of the input; otherwise, the mutation will return an error.

<a id="plugin-changes"></a>

## Plugin changes

<a id="removed-plugins"></a>

### Removed plugins

The invoicing plugin was removed. There is no direct replacement available. See [https://github.com/saleor/examples/tree/main/example-app-invoices](https://github.com/saleor/examples/tree/main/example-app-invoices) for an example of a replacement app.

The "Stripe (Deprecated)" (`mirumee.payments.stripe`) plugin was removed. Use the "Stripe" (`saleor.payments.stripe`) plugin instead or see [https://github.com/saleor/saleor-app-payment-stripe](https://github.com/saleor/saleor-app-payment-stripe) for an example of a replacement app.

<a id="deprecated-managerperform_mutation-method"></a>

### Deprecated `manager.perform_mutation` method

Note: Affects open-source Saleor users with custom plugins overriding the `perform_mutation` plugin method.

As part of gradual plugin deprecation, Saleor 3.20 deprecates the `perform_mutation` plugin hook. This method allowed to customize the mutation execution flow and call arbitrary logic during mutation execution. From now on, we recommend building such customizations via apps by reacting to webhooks triggered by mutations. The `perform_mutation` plugin method will be removed in Saleor 3.21.

<a id="deprecated-webhookplugin-and-webhook-related-plugin-hooks"></a>

### Deprecated `WebhookPlugin` and webhook-related plugin hooks

Note: Affects open-source Saleor users with custom plugins that override webhook-related plugin hooks (see the list below).

The `WebhookPlugin` is deprecated and will be removed in a future release, as all webhook-related functionality will be moved from plugin to core modules. Rationale behind this change is that webhooks are essential part of Saleor and should be available out of the box, without the need for additional plugins. As a result, multiple plugin manager hooks will be deprecated, as they are only used by the `WebhookPlugin`.

If you have custom plugins that override any of the following plugin hooks, you should migrate your custom logic to Saleor App that listens to webhooks.

The list of deprecated plugin hooks is as follows:

-   `account_change_email_requested`
-   `account_confirmation_requested`
-   `account_confirmed`
-   `account_delete_requested`
-   `account_deleted`
-   `account_email_changed`
-   `account_set_password_requested`
-   `address_created`
-   `address_deleted`
-   `address_updated`
-   `app_deleted`
-   `app_installed`
-   `app_status_changed`
-   `app_updated`
-   `attribute_created`
-   `attribute_deleted`
-   `attribute_updated`
-   `attribute_value_created`
-   `attribute_value_deleted`
-   `attribute_value_updated`
-   `category_created`
-   `category_deleted`
-   `category_updated`
-   `channel_created`
-   `channel_deleted`
-   `channel_metadata_updated`
-   `channel_status_changed`
-   `channel_updated`
-   `checkout_created`
-   `checkout_fully_paid`
-   `checkout_metadata_updated`
-   `checkout_updated`
-   `collection_created`
-   `collection_deleted`
-   `collection_metadata_updated`
-   `collection_updated`
-   `customer_created`
-   `customer_deleted`
-   `customer_metadata_updated`
-   `customer_updated`
-   `draft_order_created`
-   `draft_order_deleted`
-   `draft_order_updated`
-   `event_delivery_retry`
-   `fulfillment_approved`
-   `fulfillment_canceled`
-   `fulfillment_created`
-   `fulfillment_metadata_updated`
-   `get_shipping_methods_for_checkout`
-   `get_taxes_for_checkout`
-   `get_taxes_for_order`
-   `gift_card_created`
-   `gift_card_deleted`
-   `gift_card_export_completed`
-   `gift_card_metadata_updated`
-   `gift_card_status_changed`
-   `gift_card_updated`
-   `invoice_delete`
-   `invoice_sent`
-   `list_stored_payment_methods`
-   `menu_created`
-   `menu_deleted`
-   `menu_item_created`
-   `menu_item_deleted`
-   `menu_item_updated`
-   `menu_updated`
-   `order_bulk_created`
-   `order_cancelled`
-   `order_created`
-   `order_expired`
-   `order_fulfilled`
-   `order_fully_paid`
-   `order_fully_refunded`
-   `order_metadata_updated`
-   `order_paid`
-   `order_refunded`
-   `order_updated`
-   `page_created`
-   `page_deleted`
-   `page_type_created`
-   `page_type_deleted`
-   `page_type_updated`
-   `page_updated`
-   `payment_gateway_initialize_session`
-   `payment_gateway_initialize_tokenization`
-   `payment_method_initialize_tokenization`
-   `payment_method_process_tokenization`
-   `permission_group_created`
-   `permission_group_deleted`
-   `permission_group_updated`
-   `product_created`
-   `product_deleted`
-   `product_export_completed`
-   `product_media_created`
-   `product_media_deleted`
-   `product_media_updated`
-   `product_metadata_updated`
-   `product_updated`
-   `product_variant_back_in_stock`
-   `product_variant_created`
-   `product_variant_deleted`
-   `product_variant_metadata_updated`
-   `product_variant_out_of_stock`
-   `product_variant_stock_updated`
-   `product_variant_updated`
-   `promotion_created`
-   `promotion_deleted`
-   `promotion_ended`
-   `promotion_rule_created`
-   `promotion_rule_deleted`
-   `promotion_rule_updated`
-   `promotion_started`
-   `promotion_updated`
-   `sale_created`
-   `sale_deleted`
-   `sale_updated`
-   `shipping_price_created`
-   `shipping_price_deleted`
-   `shipping_price_updated`
-   `shipping_zone_created`
-   `shipping_zone_deleted`
-   `shipping_zone_metadata_updated`
-   `shipping_zone_updated`
-   `shop_metadata_updated`
-   `staff_created`
-   `staff_deleted`
-   `staff_set_password_requested`
-   `staff_updated`
-   `stored_payment_method_request_delete`
-   `thumbnail_created`
-   `transaction_cancelation_requested`
-   `transaction_charge_requested`
-   `transaction_initialize_session`
-   `transaction_item_metadata_updated`
-   `transaction_process_session`
-   `transaction_refund_requested`
-   `voucher_code_export_completed`
-   `voucher_codes_created`
-   `voucher_codes_deleted`
-   `voucher_created`
-   `voucher_deleted`
-   `voucher_metadata_updated`
-   `voucher_updated`
-   `warehouse_created`
-   `warehouse_deleted`
-   `warehouse_metadata_updated`
-   `warehouse_updated`

<a id="change_user_address-method-removed-from-plugin-manager"></a>

### `change_user_address` method removed from Plugin Manager

Note: Affects open-source Saleor users with custom plugins overriding the `change_user_address` plugin method.

The `change_user_address` method has been removed from the Plugin Manager and is no longer available.

<a id="changed-order-status-behavior-for-waiting_for_approval-fulfillments"></a>

## Changed order status behavior for `WAITING_FOR_APPROVAL` fulfillments

In previous versions, orders that had any unapproved fulfillments and no returns, were given status `PARTIALLY_FULFILLED`. This happened even when no items were actually fulfilled. Currently, fulfillments with `WAITING_FOR_APPROVAL` status are excluded from determination of order status. Adding unapproved fulfillment to order will no longer result in a status change. Order can only receive `PARTIALLY_FULFILLED` status if there exists `FULFILLED` fulfillment and there are some items that are either unfulfilled or awaiting approval.

<a id="changes-to-discount-propagation-in-orders-created-from-checkout"></a>

## Changes to discount propagation in orders created from checkout

In Saleor 3.21, discount propagation for orders created from checkout has been standardized to match the behavior of draft orders, ensuring consistent handling of vouchers and promotions across all order types. Below, you'll find details on how discount propagation now behaves for different types of discounts.

<a id="voucher-handling-before-version-321"></a>

### Voucher handling before version 3.21

In versions prior to **3.21**, how vouchers were applied and represented depended on how the order was created:

| Order source | `SPECIFIC_PRODUCT` voucher | `ENTIRE_ORDER` with `apply-once-per-order` voucher |
| --- | --- | --- |
| Checkout | Represented as `OrderDiscount` in `order.discounts` | Represented as `OrderDiscount` in `order.discounts` |
| Draft order | **Not** included in `order.discounts` | **Not** included in `order.discounts` |

Additionally, **percentage-based vouchers** applied during checkout were automatically **converted to fixed-value discounts**, while draft orders retained the original **percentage type**:

| Order source | `SPECIFIC_PRODUCT` (percentage) | `ENTIRE_ORDER` with `apply-once-per-order` (percentage) |
| --- | --- | --- |
| Checkout | Converted to `FIXED` | Converted to `FIXED` |
| Draft order | Retained as `PERCENTAGE` | Retained as `PERCENTAGE` |

<a id="changes-introduced-in-version-321"></a>

#### Changes introduced in version 3.21

Starting in **version 3.21**, voucher propagation is consistent across all order types:

-   All orders created via checkout now use `OrderLineDiscount` objects for both `SPECIFIC_PRODUCT` and `ENTIRE_ORDER` (with `apply-once-per-order`) vouchers — same as draft orders.
-   Percentage-based vouchers **are no longer converted** to fixed-value discounts. The original `PERCENTAGE` type is preserved and correctly represented in the API.

<a id="changes-to-unitdiscountreason-and-unitdiscountamount-for-orders-created-from-checkout"></a>

### Changes to `unitDiscountReason` and `unitDiscount.amount` for orders created from checkout

Previously, for orders created via checkout using a `Voucher:ENTIRE_ORDER`, the discount was represented in two ways:

-   As an `OrderDiscount` object in `order.discounts`
-   As a proportional amount in each line's `unitDiscount.amount`, along with a corresponding `unitDiscountReason`

<a id="before-version-321"></a>

#### Before version 3.21

| Order source | `unitDiscountReason` | `unitDiscount.amount` |
| --- | --- | --- |
| Checkout | Included reason for `ENTIRE_ORDER` discount | Included line-level portion of the `ENTIRE_ORDER` discount |
| Draft order | Reason for `ENTIRE_ORDER` discount not included | Line-level portion of the `ENTIRE_ORDER` discount not included |

This also applied to discounts from active `OrderPromotion`s:

| Order source | `unitDiscountReason` | `unitDiscount.amount` |
| --- | --- | --- |
| Checkout | Included reason for promotion discount | Included line-level portion of promotion discount |
| Draft order | Reason for promotion discount not included | Line-level portion of promotion discount not included |

<a id="after-version-321"></a>

#### After version 3.21

As of **version 3.21**, `unitDiscountReason` and `unitDiscount.amount` **no longer include** information related to:

-   `Voucher:ENTIRE_ORDER` discounts
-   `OrderPromotion` discounts

This behavior is now consistent with how draft orders work.

<a id="backward-compatibility-uselegacylinediscountpropagation-feature-flag"></a>

### Backward compatibility: `useLegacyLineDiscountPropagation` feature flag

To maintain compatibility with existing integrations, version 3.21 introduces a **feature flag**: `useLegacyLineDiscountPropagation`, which allows you to configure the discount propagation behavior per channel.

This flag controls how voucher and promotion discounts appear in the API for **orders created via checkout**.

<a id="how-discounts-are-represented-based-on-the-flag"></a>

#### How discounts are represented based on the flag

| Checkout order + voucher | 3.20 | 3.21 + Legacy propagation **enabled** | 3.21 + Legacy propagation **disabled** |
| --- | --- | --- | --- |
| `SPECIFIC_PRODUCT` | `OrderDiscount` (in `order.discounts`) | `OrderDiscount` (in `order.discounts`) | `OrderLineDiscount` (in `OrderLine.discounts`) only |
| `ENTIRE_ORDER` + `apply-once-per-order` | `OrderDiscount` (in `order.discounts`) | `OrderDiscount` (in `order.discounts`) | `OrderLineDiscount` (in `OrderLine.discounts`) only |

<a id="discount-type-fixed-vs-percentage-based-on-the-flag"></a>

#### Discount type (`FIXED` vs `PERCENTAGE`) based on the flag

| Checkout order + percentage voucher | 3.20 | 3.21 + Legacy propagation **enabled** | 3.21 + Legacy propagation **disabled** |
| --- | --- | --- | --- |
| `SPECIFIC_PRODUCT` (percentage) | Converted to `FIXED` | Converted to `FIXED` | Retained as `PERCENTAGE` |
| `ENTIRE_ORDER` (percentage) | Converted to `FIXED` | Converted to `FIXED` | Retained as `PERCENTAGE` |

<a id="unitdiscountreason-and-unitdiscountamount-based-on-the-flag"></a>

#### `unitDiscountReason` and `unitDiscount.amount` based on the flag

| Checkout order + voucher type | 3.20 | 3.21 + Legacy propagation **enabled** | 3.21 + Legacy propagation **disabled** |
| --- | --- | --- | --- |
| `Voucher:ENTIRE_ORDER` | Included in `unitDiscountReason` & `unitDiscount.amount` | Included in `unitDiscountReason` & `unitDiscount.amount` | Not included in `unitDiscountReason` & `unitDiscount.amount` |
| `OrderPromotion` | Included in `unitDiscountReason` & `unitDiscount.amount` | Included in `unitDiscountReason` & `unitDiscount.amount` | Not included in `unitDiscountReason` & `unitDiscount.amount` |

<a id="default-behavior"></a>

#### Default behavior

-   **Existing channels**: `useLegacyLineDiscountPropagation` is **enabled** by default
-   **New channels**: the flag is **disabled** by default, unless explicitly set otherwise

note

Changing the `useLegacyLineDiscountPropagation` flag only affects **orders created after upgrading to version 3.21**.  
It does **not** retroactively change:

-   Discount types for vouchers already converted from `PERCENTAGE` to `FIXED`
-   `unitDiscountReason` and `unitDiscount.amount` fields on existing orders

<a id="creating-new-channel-with-legacy-flow-enabled"></a>

### Creating new channel with legacy flow enabled

If you need to create a new channel with the **legacy voucher propagation** flow active, you must explicitly set `useLegacyLineDiscountPropagation` in the `channelCreate` mutation input:

```graphql
mutation {
  channelCreate(
    input: {
      name: "New channel"
      slug: "new-channel"
      currencyCode: USD
      defaultCountry: PL
      orderSettings: { useLegacyLineDiscountPropagation: true }
    }
  ) {
    channel {
      id
    }
    errors {
      field
      message
    }
  }
}
```

By default, new channels have legacy propagation **disabled** unless this field is explicitly set to `true`.

<a id="example"></a>

#### Example

The examples below show how order line discounts are represented in the GraphQL response, depending on the `useLegacyLineDiscountPropagation` configuration.

<a id="with-uselegacylinediscountpropagation-enabled"></a>

##### With `useLegacyLineDiscountPropagation` enabled:

```json
{
  "data": {
    "order": {
      "id": "T3JkZXI6NzJiM2NhNjktNzJmYS00ZjQ5LWE3ZGMtOTgxZGI4MDIwNmJj",
      "discounts": [
        {
          "id": "T3JkZXJEaXNjb3VudDo1NjhhMGE5OS1hY2Y1LTQ1NzctYjE2YS0zNjFlMjU3MTAyYmE=",
          "name": "My voucher",
          "type": "VOUCHER",
          "valueType": "FIXED",
          "value": 11,
          "total": {
            "amount": 11,
            "currency": "USD"
          },
          "amount": {
            "amount": 11,
            "currency": "USD"
          }
        }
      ],
      "lines": [
        {
          "unitDiscount": { "amount": 3.67 },
          "discounts": []
        },
        {
          "unitDiscount": { "amount": 0 },
          "discounts": []
        }
      ]
    }
  }
}
```

<a id="with-uselegacylinediscountpropagation-disabled"></a>

##### With `useLegacyLineDiscountPropagation` disabled:

```json
{
  "data": {
    "order": {
      "id": "T3JkZXI6NzJiM2NhNjktNzJmYS00ZjQ5LWE3ZGMtOTgxZGI4MDIwNmJj",
      "discounts": [],
      "lines": [
        {
          "unitDiscount": { "amount": 3.67 },
          "discounts": [
            {
              "name": "My voucher",
              "type": "VOUCHER",
              "valueType": "FIXED",
              "value": 11,
              "unit": { "amount": 3.67 },
              "total": { "amount": 11 }
            }
          ]
        },
        {
          "unitDiscount": { "amount": 0 },
          "discounts": []
        }
      ]
    }
  }
}
```

<a id="how-to-migrate-to-the-new-flow"></a>

### How to migrate to the new flow

To fully support the updated propagation model:

-   Extend webhook subscriptions to include: `order.lines[].discounts[]`
-   Update all queries to retrieve `OrderLineDiscount` data from `order.lines[].discounts[]`
-   Adjust internal logic to rely on line-level discount data
-   Disable legacy propagation using the following mutation:

```graphql
mutation {
  channelUpdate(
    id: "<channel-id>"
    input: { orderSettings: { useLegacyLineDiscountPropagation: false } }
  ) {
    channel {
      orderSettings {
        useLegacyLineDiscountPropagation
      }
    }
  }
}
```

<a id="google-cloud-platform-gcp-configuration-changes"></a>

## Google Cloud Platform (GCP): Configuration Changes

The environment variable `GS_MEDIA_BUCKET_NAME` [no longer applies](https://github.com/saleor/saleor/pull/17660) for files that may contain sensitive information. The following steps should be taken:

1.  Keep the original configuration for `GS_MEDIA_BUCKET_NAME` - it will still be used for public media files, such as product images.
2.  Set a new environment variable `GS_MEDIA_PRIVATE_BUCKET_NAME` - it should point to a private bucket that's not exposed to the Internet.

For more information, see [Storing Files on Google Cloud Storage (GCS)](/setup/media-gcs.md#environment-variables).

<a id="changes-in-transactioncancelationrequestedactionamount"></a>

## Changes in TransactionCancelationRequested.action.amount

Previously, the `action.amount` field in the `TransactionCancelationRequested` subscription generated by Saleor was always set to `null`. Starting with Saleor 3.21, this field will contain the amount requested by the user. If no amount is provided by the user, Saleor will use the maximum available amount from `transaction.authorizedAmount` as a fallback.

<a id="changes-to-processing-the-amount-field-in-synchronous-transaction-webhooks"></a>

## Changes to processing the amount field in synchronous transaction webhooks

In Saleor 3.21, the `amount` field in responses to the following synchronous webhooks becomes optional:

-   `TRANSACTION_CHARGE_REQUESTED`
-   `TRANSACTION_REFUND_REQUESTED`
-   `TRANSACTION_CANCELATION_REQUESTED`
-   `TRANSACTION_INITIALIZE_SESSION`
-   `TRANSACTION_PROCESS_SESSION`

If the `amount` field is omitted in the response, Saleor will automatically use the `amount` requested in the original webhook payload as the default.

<a id="changes-in-handling-email-input-in-checkoutcreate-and-checkoutemailupdate-mutations"></a>

## Changes in handling `email` input in `checkoutCreate` and `checkoutEmailUpdate` mutations

Previously, when calling `checkoutCreate` or `checkoutEmailUpdate` as a logged-in user, the `email` input field was ignored. Instead, the email associated with the user’s account was always used. As a result, any newly created checkouts or orders for logged-in users were always tied to the user's account email, regardless of the input provided.

Starting from version 3.21, this behavior has changed: if an `email` is explicitly provided as input, it will be respected and used instead of the user account's email.

<a id="updated-behavior"></a>

### Updated behavior:

-   `checkoutCreate` (logged-in user): the optional `email` input will now be used to assign the email to the checkout.
-   `checkoutEmailUpdate` (logged-in user): the `email` input will now be used to assign the email to the checkout.

<a id="comparison-of-behavior"></a>

### Comparison of behavior:

| Mutation | 3.20 behavior | 3.21 behavior |
| --- | --- | --- |
| `checkoutCreate` (logged-in user) when called with `email: from.input@email` | Field `checkout.email` returns `account.user@email` | Field `checkout.email` returns `from.input@email` |
| `checkoutEmailUpdate` (logged-in user) when called with `email: from.input@email` | Field `checkout.email` returns `account.user@email` | Field `checkout.email` returns `from.input@email` |

<a id="change-in-default-ordersettingsmarkaspaidstrategy"></a>

## Change in default `orderSettings.markAsPaidStrategy`

When creating new channels in Saleor 3.21, the default value for `paymentSettings.defaultTransactionFlowStrategy` is now set to [`MarkAsPaidStrategyEnum.TRANSACTION_FLOW`](/api-reference/channels/enums/mark-as-paid-strategy-enum.md#markaspaidstrategyenumtransaction_flow).

<a id="behavior-change-in-accountregister-mutation"></a>

## Behavior change in `accountRegister` mutation

<a id="before-saleor-321"></a>

#### Before Saleor 3.21

When calling `accountRegister` for an already existing user, Saleor returned an error response, for example:

```json
{
  "data": {
    "accountRegister": {
      "errors": [
        {
          "field": "email",
          "message": "User with this Email already exists.",
          "code": "UNIQUE"
        }
      ],
      "user": null
    }
  },
  "extensions": {
    "cost": {
      "requestedQueryCost": 1,
      "maximumAvailable": 50000
    }
  }
}
```

<a id="from-saleor-321"></a>

#### From Saleor 3.21+

To prevent user enumeration attacks, the `accountRegister` mutation no longer returns an error when the user already exists. Instead, it always returns a user object. The id field is always set to an empty string:

```json
{
  "data": {
    "accountRegister": {
      "errors": [],
      "user": {
        "id": "",
        "email": "existing-user@example.com"
      }
    }
  },
  "extensions": {
    "cost": {
      "requestedQueryCost": 0,
      "maximumAvailable": 50000
    }
  }
}
```

If your application needs to verify whether a user with the given email already exists in Saleor, you must perform an additional [query](/api-reference/users/queries/user.md). This step needs to be performed by app with proper permissions.
